Printable Maze Puzzle — MUI X Charts

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 MUI X Charts

Renders

JavaScript source (MUI X Charts)

// anyplot.ai
// maze-printable: Printable Maze Puzzle
// Library: muix 7.29.1 | JavaScript 22.23.2
// Quality: 88/100 | Created: 2026-09-05
//# anyplot-orientation: square
// anyplot.ai
// maze-printable: Printable Maze Puzzle
// Library: MUI X Charts | React | Node 22
// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.
// Quality: pending | Created: 2026-09-05

import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { ScatterChart } from "@mui/x-charts/ScatterChart";
import { useDrawingArea } from "@mui/x-charts/hooks";

const t = window.ANYPLOT_TOKENS;

// The maze itself is a print artifact — "black walls on white background for
// maximum contrast and ink efficiency" per spec — so its ink/paper colors
// stay fixed across themes, same convention as the maze-circular muix
// implementation. Only the surrounding page and title follow ANYPLOT_THEME.
const PAPER = "#FAF8F1";
const INK = "#1A1A17";
const GOAL_GREEN = "#009E73";

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

const rand = mulberry32(42);

// --- Maze topology: a ROWS x COLS grid, start top-left, goal bottom-right --
const ROWS = 22;
const COLS = 22;

const cellId = (r, c) => r * COLS + c;
const edgeKey = (a, b) => (a < b ? `${a}|${b}` : `${b}|${a}`);

function neighborsOf(r, c) {
  const result = [];
  if (r > 0) result.push([r - 1, c]);
  if (r < ROWS - 1) result.push([r + 1, c]);
  if (c > 0) result.push([r, c - 1]);
  if (c < COLS - 1) result.push([r, c + 1]);
  return result;
}

// Randomized depth-first search (recursive backtracker), run iteratively to
// avoid deep call stacks. Every carved passage links a visited cell to a
// brand-new one, so the carved edges form a spanning tree over the grid —
// exactly one path connects any two cells, including start and goal, which
// is what "guarantee exactly one solution" requires.
const passages = new Set();
const visited = new Set([cellId(0, 0)]);
const stack = [[0, 0]];
while (stack.length > 0) {
  const [r, c] = stack[stack.length - 1];
  const candidates = neighborsOf(r, c).filter(
    ([nr, nc]) => !visited.has(cellId(nr, nc)),
  );
  if (candidates.length === 0) {
    stack.pop();
    continue;
  }
  const [nr, nc] = candidates[Math.floor(rand() * candidates.length)];
  passages.add(edgeKey(cellId(r, c), cellId(nr, nc)));
  visited.add(cellId(nr, nc));
  stack.push([nr, nc]);
}

const isPassage = (r1, c1, r2, c2) =>
  passages.has(edgeKey(cellId(r1, c1), cellId(r2, c2)));

// Shortest path length over the spanning tree, i.e. THE solution length (the
// carved passages form a tree, so start->goal has exactly one route — no
// search heuristics needed, a plain BFS finds it). Reported in the footnote
// as a difficulty cue without ever drawing the path itself, so the puzzle
// stays unspoiled.
function solutionLength() {
  const startId = cellId(0, 0);
  const goalId = cellId(ROWS - 1, COLS - 1);
  const cameFrom = new Map([[startId, null]]);
  const queue = [startId];
  for (let head = 0; head < queue.length; head++) {
    const current = queue[head];
    if (current === goalId) break;
    const r = Math.floor(current / COLS);
    const c = current % COLS;
    for (const [nr, nc] of neighborsOf(r, c)) {
      const next = cellId(nr, nc);
      if (!cameFrom.has(next) && isPassage(r, c, nr, nc)) {
        cameFrom.set(next, current);
        queue.push(next);
      }
    }
  }
  let steps = 0;
  for (let node = goalId; node !== startId; steps++) {
    node = cameFrom.get(node);
  }
  return steps;
}

const SOLUTION_STEPS = solutionLength();

// --- Why ScatterChart (not LineChart/BarChart) hosts this maze --------------
// MUI X community has no grid-of-walls / graph-maze primitive, so some
// canvas-hosting workaround is unavoidable here. ScatterChart is deliberately
// the thinnest option: a single anonymous point satisfies its `series` prop
// with nothing left over to suppress, its axes take an explicit numeric
// domain (0..COLS / 0..ROWS) with no forced ticks or gridlines, and
// `useDrawingArea()` returns the exact inset rectangle MUI already computed
// for margins/aspect — so the maze inherits the chart's own responsive layout
// math for free. LineChart/BarChart would force a categorical or continuous
// axis with visible tick/gridline defaults that fight the print-artifact
// look and need more overrides to hide.
//
// Draws the whole puzzle as raw SVG paths sized off that drawing area — MUI X
// owns layout/scaling, the maze geometry is ours (same composition pattern as
// the maze-circular muix implementation).
function MazeMark() {
  const { left, top, width, height } = useDrawingArea();
  const cellSize = Math.min(width / COLS, height / ROWS);
  const mazeWidth = cellSize * COLS;
  const mazeHeight = cellSize * ROWS;
  const originX = left + (width - mazeWidth) / 2;
  const originY = top + (height - mazeHeight) / 2;
  const px = (c) => originX + c * cellSize;
  const py = (r) => originY + r * cellSize;

  const walls = [];
  for (let r = 0; r < ROWS; r++) {
    for (let c = 0; c < COLS - 1; c++) {
      if (!isPassage(r, c, r, c + 1)) {
        const x = px(c + 1);
        walls.push(`M ${x} ${py(r)} L ${x} ${py(r + 1)}`);
      }
    }
  }
  for (let r = 0; r < ROWS - 1; r++) {
    for (let c = 0; c < COLS; c++) {
      if (!isPassage(r, c, r + 1, c)) {
        const y = py(r + 1);
        walls.push(`M ${px(c)} ${y} L ${px(c + 1)} ${y}`);
      }
    }
  }

  // Border noticeably heavier than the interior walls: a clear weight
  // hierarchy (frame > corridor) instead of one uniform line thickness, and
  // both are sized up from a flat pixel constant to scale with the cell so
  // the puzzle still reads at small thumbnail sizes.
  const wallStroke = Math.max(4, cellSize * 0.075);
  const borderStroke = wallStroke * 1.75;
  const borderRadius = cellSize * 0.12;

  return (
    <g>
      <rect
        x={originX}
        y={originY}
        width={mazeWidth}
        height={mazeHeight}
        fill={PAPER}
        stroke={INK}
        strokeWidth={borderStroke}
        rx={borderRadius}
        ry={borderRadius}
      />
      {walls.map((d, i) => (
        <path
          key={i}
          d={d}
          stroke={INK}
          strokeWidth={wallStroke}
          strokeLinecap="square"
          fill="none"
        />
      ))}
      <text
        x={px(0.5)}
        y={py(0.5)}
        fontSize={cellSize * 0.55}
        fontWeight={700}
        fill={INK}
        textAnchor="middle"
        dominantBaseline="central"
      >
        S
      </text>
      <circle
        cx={px(COLS - 0.5)}
        cy={py(ROWS - 0.5)}
        r={cellSize * 0.4}
        fill={GOAL_GREEN}
      />
      <text
        x={px(COLS - 0.5)}
        y={py(ROWS - 0.5)}
        fontSize={cellSize * 0.5}
        fontWeight={700}
        fill={PAPER}
        textAnchor="middle"
        dominantBaseline="central"
      >
        G
      </text>
      {/* Outside the paper card, on the page background, so — unlike the
          fixed ink/paper maze above it — this footnote must follow
          ANYPLOT_THEME or it goes invisible on the dark page. */}
      <text
        x={originX}
        y={originY + mazeHeight + 30}
        fontSize={16}
        fill={t.inkSoft}
        textAnchor="start"
      >
        {ROWS}×{COLS} grid · seed 42 · {SOLUTION_STEPS}-step solution
      </text>
    </g>
  );
}

const TITLE = "maze-printable · javascript · muix · anyplot.ai";
const TITLE_HEIGHT = 64;

export default function Chart() {
  const { width, height } = window.ANYPLOT_SIZE;
  const chartHeight = height - TITLE_HEIGHT;
  const margin = 48;

  return (
    <Box
      sx={{
        width,
        height,
        bgcolor: t.pageBg,
        display: "flex",
        flexDirection: "column",
      }}
    >
      <Typography
        sx={{
          color: t.ink,
          fontSize: 30,
          fontWeight: 500,
          textAlign: "center",
          lineHeight: 1.2,
          pt: "16px",
          height: TITLE_HEIGHT,
          fontFamily: "inherit",
        }}
      >
        {TITLE}
      </Typography>
      <Box
        sx={{
          flex: 1,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
        }}
      >
        <ScatterChart
          width={width}
          height={chartHeight}
          skipAnimation
          disableVoronoi
          series={[
            { id: "maze", type: "scatter", data: [{ x: 0, y: 0, id: "c" }] },
          ]}
          xAxis={[
            {
              scaleType: "linear",
              min: 0,
              max: COLS,
              disableTicks: true,
              disableLine: true,
            },
          ]}
          yAxis={[
            {
              scaleType: "linear",
              min: 0,
              max: ROWS,
              disableTicks: true,
              disableLine: true,
            },
          ]}
          topAxis={null}
          bottomAxis={null}
          leftAxis={null}
          rightAxis={null}
          margin={{ top: margin, bottom: margin, left: margin, right: margin }}
          slots={{ scatter: MazeMark }}
          slotProps={{ legend: { hidden: true } }}
        />
      </Box>
    </Box>
  );
}

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/maze-printable/muix/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": "muix",
  "page": "https://anyplot.ai/maze-printable/javascript/muix",
  "hub": "https://anyplot.ai/maze-printable",
  "code_json": "https://api.anyplot.ai/specs/maze-printable/muix/code",
  "spec_json": "https://api.anyplot.ai/specs/maze-printable",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/maze-printable/javascript/muix/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/maze-printable/javascript/muix/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/maze-printable/javascript/muix/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/maze-printable/javascript/muix/plot-dark.html",
  "quality_score": 88.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Printable Maze Puzzle on anyplot.ai.

Other implementations