Printable Maze Puzzle — D3.js

A rectangular maze puzzle visualization with clearly marked start and goal positions. The maze is algorithmically generated to guarantee exactly one solution path from start to finish. The clean black-and-white design is optimized for printing, allowing users to solve the puzzle with a pen or pencil.

Printable Maze Puzzle rendered with D3.js

Renders

JavaScript source (D3.js)

// anyplot.ai
// maze-printable: Printable Maze Puzzle
// Library: d3 7.9.0 | JavaScript 22.23.2
// Quality: 90/100 | Created: 2026-09-05

//# anyplot-orientation: square
const t = window.ANYPLOT_TOKENS;
const { width, height } = window.ANYPLOT_SIZE;

// --- Deterministic PRNG (mulberry32) — browser has no seeded Math.random ----
const mulberry32 = (seed) => () => {
  seed |= 0;
  seed = (seed + 0x6d2b79f5) | 0;
  let z = Math.imul(seed ^ (seed >>> 15), 1 | seed);
  z = (z + Math.imul(z ^ (z >>> 7), 61 | z)) ^ z;
  return ((z ^ (z >>> 14)) >>> 0) / 4294967296;
};
const rng = mulberry32(20260905);

// --- Maze generation: randomized depth-first search (recursive backtracker) -
// Guarantees a "perfect maze" — exactly one path between any two cells.
const cols = 22;
const rows = 22;
const cells = Array.from({ length: cols * rows }, () => ({
  top: true,
  right: true,
  bottom: true,
  left: true,
  visited: false,
}));
const index = (col, row) => row * cols + col;

const stack = [{ col: 0, row: 0 }];
cells[index(0, 0)].visited = true;
while (stack.length > 0) {
  const { col, row } = stack[stack.length - 1];
  const candidates = [
    { col, row: row - 1, dir: "top", opp: "bottom" },
    { col: col + 1, row, dir: "right", opp: "left" },
    { col, row: row + 1, dir: "bottom", opp: "top" },
    { col: col - 1, row, dir: "left", opp: "right" },
  ].filter(
    (n) =>
      n.col >= 0 &&
      n.col < cols &&
      n.row >= 0 &&
      n.row < rows &&
      !cells[index(n.col, n.row)].visited,
  );

  if (candidates.length === 0) {
    stack.pop();
    continue;
  }
  const next = candidates[Math.floor(rng() * candidates.length)];
  const current = cells[index(col, row)];
  const neighbor = cells[index(next.col, next.row)];
  current[next.dir] = false;
  neighbor[next.opp] = false;
  neighbor.visited = true;
  stack.push({ col: next.col, row: next.row });
}

// --- Layout -------------------------------------------------------------
const margin = { top: 120, right: 70, bottom: 80, left: 70 };
const availableWidth = width - margin.left - margin.right;
const availableHeight = height - margin.top - margin.bottom;
const cellSize = Math.floor(
  Math.min(availableWidth / cols, availableHeight / rows),
);
const mazeWidth = cellSize * cols;
const mazeHeight = cellSize * rows;
const offsetX = margin.left + (availableWidth - mazeWidth) / 2;
const offsetY = margin.top + (availableHeight - mazeHeight) / 2;

// --- SVG mount ------------------------------------------------------------
const svg = d3
  .select("#container")
  .append("svg")
  .attr("width", width)
  .attr("height", height);
const g = svg.append("g").attr("transform", `translate(${offsetX},${offsetY})`);

// --- Walls: merge same-direction adjacent segments into fewer path elements
// via d3.path, and give the outer border a heavier stroke than the interior
// walls so the puzzle frame reads as a clear visual hierarchy (DE-01) --------
const innerWallPaths = [];

// Interior horizontal grid lines (between row r-1 and row r), run-length
// encoded with d3.path so a contiguous stretch of wall becomes one subpath.
d3.range(1, rows).forEach((r) => {
  const p = d3.path();
  let runStart = null;
  d3.range(cols + 1).forEach((col) => {
    const hasWall = col < cols && cells[index(col, r - 1)].bottom;
    if (hasWall && runStart === null) runStart = col;
    if (!hasWall && runStart !== null) {
      p.moveTo(runStart * cellSize, r * cellSize);
      p.lineTo(col * cellSize, r * cellSize);
      runStart = null;
    }
  });
  const d = p.toString();
  if (d) innerWallPaths.push(d);
});

// Interior vertical grid lines (between col c-1 and col c), same run-length
// merge along the column.
d3.range(1, cols).forEach((c) => {
  const p = d3.path();
  let runStart = null;
  d3.range(rows + 1).forEach((row) => {
    const hasWall = row < rows && cells[index(c - 1, row)].right;
    if (hasWall && runStart === null) runStart = row;
    if (!hasWall && runStart !== null) {
      p.moveTo(c * cellSize, runStart * cellSize);
      p.lineTo(c * cellSize, row * cellSize);
      runStart = null;
    }
  });
  const d = p.toString();
  if (d) innerWallPaths.push(d);
});

g.selectAll("path.wall-inner")
  .data(innerWallPaths)
  .join("path")
  .attr("class", "wall-inner")
  .attr("d", (d) => d)
  .attr("fill", "none")
  .attr("stroke", t.ink)
  .attr("stroke-width", 3)
  .attr("stroke-linecap", "square");

// Outer border: the maze boundary is always fully closed (the DFS carver
// never removes a perimeter wall), so it renders as a single heavier-stroke
// rectangle that frames the puzzle.
const borderPath = d3.path();
borderPath.moveTo(0, 0);
borderPath.lineTo(mazeWidth, 0);
borderPath.lineTo(mazeWidth, mazeHeight);
borderPath.lineTo(0, mazeHeight);
borderPath.closePath();

g.append("path")
  .attr("class", "wall-border")
  .attr("d", borderPath.toString())
  .attr("fill", "none")
  .attr("stroke", t.ink)
  .attr("stroke-width", 6)
  .attr("stroke-linejoin", "miter");

// --- Start / goal markers ---------------------------------------------------
const markers = [
  { col: 0, row: 0, label: "S", name: "Start", color: t.palette[0] },
  {
    col: cols - 1,
    row: rows - 1,
    label: "G",
    name: "Goal",
    color: t.palette[1],
  },
];

const markerGroup = g
  .selectAll("g.marker")
  .data(markers)
  .join("g")
  .attr("class", "marker")
  .attr(
    "transform",
    (d) =>
      `translate(${d.col * cellSize + cellSize / 2},${d.row * cellSize + cellSize / 2})`,
  );

markerGroup
  .append("circle")
  .attr("r", cellSize * 0.36)
  .attr("fill", (d) => d.color);
markerGroup
  .append("text")
  .attr("text-anchor", "middle")
  .attr("dominant-baseline", "central")
  .attr("fill", t.pageBg)
  .style("font-size", `${Math.round(cellSize * 0.42)}px`)
  .style("font-weight", "700")
  .text((d) => d.label);

// --- Title ------------------------------------------------------------------
svg
  .append("text")
  .attr("x", width / 2)
  .attr("y", 56)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "26px")
  .style("font-weight", "600")
  .text("maze-printable · javascript · d3 · anyplot.ai");

svg
  .append("text")
  .attr("x", width / 2)
  .attr("y", 88)
  .attr("text-anchor", "middle")
  .attr("fill", t.inkSoft)
  .style("font-size", "16px")
  .text(`${cols} × ${rows} grid · single guaranteed solution path`);

// --- Legend -----------------------------------------------------------------
const legend = svg
  .append("g")
  .attr(
    "transform",
    `translate(${width / 2 - 130},${height - margin.bottom / 2 + 8})`,
  );

const legendItems = legend
  .selectAll("g.legend-item")
  .data(markers)
  .join("g")
  .attr("class", "legend-item")
  .attr("transform", (_, i) => `translate(${i * 140},0)`);

legendItems
  .append("circle")
  .attr("r", 12)
  .attr("cy", -6)
  .attr("fill", (d) => d.color);
legendItems
  .append("text")
  .attr("x", 22)
  .attr("y", -1)
  .attr("fill", t.inkSoft)
  .style("font-size", "16px")
  .text((d) => d.name);

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/maze-printable/d3/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "maze-printable",
  "language": "javascript",
  "library": "d3",
  "page": "https://anyplot.ai/maze-printable/javascript/d3",
  "hub": "https://anyplot.ai/maze-printable",
  "code_json": "https://api.anyplot.ai/specs/maze-printable/d3/code",
  "spec_json": "https://api.anyplot.ai/specs/maze-printable",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/maze-printable/javascript/d3/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/maze-printable/javascript/d3/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/maze-printable/javascript/d3/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/maze-printable/javascript/d3/plot-dark.html",
  "quality_score": 90.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Printable Maze Puzzle on anyplot.ai.

Other implementations