{"spec_id":"maze-printable","library":"muix","language":"javascript","code":"// anyplot.ai\n// maze-printable: Printable Maze Puzzle\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-05\n//# anyplot-orientation: square\n// anyplot.ai\n// maze-printable: Printable Maze Puzzle\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-09-05\n\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\nimport { ScatterChart } from \"@mui/x-charts/ScatterChart\";\nimport { useDrawingArea } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// The maze itself is a print artifact — \"black walls on white background for\n// maximum contrast and ink efficiency\" per spec — so its ink/paper colors\n// stay fixed across themes, same convention as the maze-circular muix\n// implementation. Only the surrounding page and title follow ANYPLOT_THEME.\nconst PAPER = \"#FAF8F1\";\nconst INK = \"#1A1A17\";\nconst GOAL_GREEN = \"#009E73\";\n\n// --- Deterministic PRNG (mulberry32) — the browser has no seeded RNG --------\nfunction mulberry32(seed) {\n  let a = seed;\n  return function random() {\n    a |= 0;\n    a = (a + 0x6d2b79f5) | 0;\n    let z = Math.imul(a ^ (a >>> 15), 1 | a);\n    z = (z + Math.imul(z ^ (z >>> 7), 61 | z)) ^ z;\n    return ((z ^ (z >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nconst rand = mulberry32(42);\n\n// --- Maze topology: a ROWS x COLS grid, start top-left, goal bottom-right --\nconst ROWS = 22;\nconst COLS = 22;\n\nconst cellId = (r, c) => r * COLS + c;\nconst edgeKey = (a, b) => (a < b ? `${a}|${b}` : `${b}|${a}`);\n\nfunction neighborsOf(r, c) {\n  const result = [];\n  if (r > 0) result.push([r - 1, c]);\n  if (r < ROWS - 1) result.push([r + 1, c]);\n  if (c > 0) result.push([r, c - 1]);\n  if (c < COLS - 1) result.push([r, c + 1]);\n  return result;\n}\n\n// Randomized depth-first search (recursive backtracker), run iteratively to\n// avoid deep call stacks. Every carved passage links a visited cell to a\n// brand-new one, so the carved edges form a spanning tree over the grid —\n// exactly one path connects any two cells, including start and goal, which\n// is what \"guarantee exactly one solution\" requires.\nconst passages = new Set();\nconst visited = new Set([cellId(0, 0)]);\nconst stack = [[0, 0]];\nwhile (stack.length > 0) {\n  const [r, c] = stack[stack.length - 1];\n  const candidates = neighborsOf(r, c).filter(\n    ([nr, nc]) => !visited.has(cellId(nr, nc)),\n  );\n  if (candidates.length === 0) {\n    stack.pop();\n    continue;\n  }\n  const [nr, nc] = candidates[Math.floor(rand() * candidates.length)];\n  passages.add(edgeKey(cellId(r, c), cellId(nr, nc)));\n  visited.add(cellId(nr, nc));\n  stack.push([nr, nc]);\n}\n\nconst isPassage = (r1, c1, r2, c2) =>\n  passages.has(edgeKey(cellId(r1, c1), cellId(r2, c2)));\n\n// Shortest path length over the spanning tree, i.e. THE solution length (the\n// carved passages form a tree, so start->goal has exactly one route — no\n// search heuristics needed, a plain BFS finds it). Reported in the footnote\n// as a difficulty cue without ever drawing the path itself, so the puzzle\n// stays unspoiled.\nfunction solutionLength() {\n  const startId = cellId(0, 0);\n  const goalId = cellId(ROWS - 1, COLS - 1);\n  const cameFrom = new Map([[startId, null]]);\n  const queue = [startId];\n  for (let head = 0; head < queue.length; head++) {\n    const current = queue[head];\n    if (current === goalId) break;\n    const r = Math.floor(current / COLS);\n    const c = current % COLS;\n    for (const [nr, nc] of neighborsOf(r, c)) {\n      const next = cellId(nr, nc);\n      if (!cameFrom.has(next) && isPassage(r, c, nr, nc)) {\n        cameFrom.set(next, current);\n        queue.push(next);\n      }\n    }\n  }\n  let steps = 0;\n  for (let node = goalId; node !== startId; steps++) {\n    node = cameFrom.get(node);\n  }\n  return steps;\n}\n\nconst SOLUTION_STEPS = solutionLength();\n\n// --- Why ScatterChart (not LineChart/BarChart) hosts this maze --------------\n// MUI X community has no grid-of-walls / graph-maze primitive, so some\n// canvas-hosting workaround is unavoidable here. ScatterChart is deliberately\n// the thinnest option: a single anonymous point satisfies its `series` prop\n// with nothing left over to suppress, its axes take an explicit numeric\n// domain (0..COLS / 0..ROWS) with no forced ticks or gridlines, and\n// `useDrawingArea()` returns the exact inset rectangle MUI already computed\n// for margins/aspect — so the maze inherits the chart's own responsive layout\n// math for free. LineChart/BarChart would force a categorical or continuous\n// axis with visible tick/gridline defaults that fight the print-artifact\n// look and need more overrides to hide.\n//\n// Draws the whole puzzle as raw SVG paths sized off that drawing area — MUI X\n// owns layout/scaling, the maze geometry is ours (same composition pattern as\n// the maze-circular muix implementation).\nfunction MazeMark() {\n  const { left, top, width, height } = useDrawingArea();\n  const cellSize = Math.min(width / COLS, height / ROWS);\n  const mazeWidth = cellSize * COLS;\n  const mazeHeight = cellSize * ROWS;\n  const originX = left + (width - mazeWidth) / 2;\n  const originY = top + (height - mazeHeight) / 2;\n  const px = (c) => originX + c * cellSize;\n  const py = (r) => originY + r * cellSize;\n\n  const walls = [];\n  for (let r = 0; r < ROWS; r++) {\n    for (let c = 0; c < COLS - 1; c++) {\n      if (!isPassage(r, c, r, c + 1)) {\n        const x = px(c + 1);\n        walls.push(`M ${x} ${py(r)} L ${x} ${py(r + 1)}`);\n      }\n    }\n  }\n  for (let r = 0; r < ROWS - 1; r++) {\n    for (let c = 0; c < COLS; c++) {\n      if (!isPassage(r, c, r + 1, c)) {\n        const y = py(r + 1);\n        walls.push(`M ${px(c)} ${y} L ${px(c + 1)} ${y}`);\n      }\n    }\n  }\n\n  // Border noticeably heavier than the interior walls: a clear weight\n  // hierarchy (frame > corridor) instead of one uniform line thickness, and\n  // both are sized up from a flat pixel constant to scale with the cell so\n  // the puzzle still reads at small thumbnail sizes.\n  const wallStroke = Math.max(4, cellSize * 0.075);\n  const borderStroke = wallStroke * 1.75;\n  const borderRadius = cellSize * 0.12;\n\n  return (\n    <g>\n      <rect\n        x={originX}\n        y={originY}\n        width={mazeWidth}\n        height={mazeHeight}\n        fill={PAPER}\n        stroke={INK}\n        strokeWidth={borderStroke}\n        rx={borderRadius}\n        ry={borderRadius}\n      />\n      {walls.map((d, i) => (\n        <path\n          key={i}\n          d={d}\n          stroke={INK}\n          strokeWidth={wallStroke}\n          strokeLinecap=\"square\"\n          fill=\"none\"\n        />\n      ))}\n      <text\n        x={px(0.5)}\n        y={py(0.5)}\n        fontSize={cellSize * 0.55}\n        fontWeight={700}\n        fill={INK}\n        textAnchor=\"middle\"\n        dominantBaseline=\"central\"\n      >\n        S\n      </text>\n      <circle\n        cx={px(COLS - 0.5)}\n        cy={py(ROWS - 0.5)}\n        r={cellSize * 0.4}\n        fill={GOAL_GREEN}\n      />\n      <text\n        x={px(COLS - 0.5)}\n        y={py(ROWS - 0.5)}\n        fontSize={cellSize * 0.5}\n        fontWeight={700}\n        fill={PAPER}\n        textAnchor=\"middle\"\n        dominantBaseline=\"central\"\n      >\n        G\n      </text>\n      {/* Outside the paper card, on the page background, so — unlike the\n          fixed ink/paper maze above it — this footnote must follow\n          ANYPLOT_THEME or it goes invisible on the dark page. */}\n      <text\n        x={originX}\n        y={originY + mazeHeight + 30}\n        fontSize={16}\n        fill={t.inkSoft}\n        textAnchor=\"start\"\n      >\n        {ROWS}×{COLS} grid · seed 42 · {SOLUTION_STEPS}-step solution\n      </text>\n    </g>\n  );\n}\n\nconst TITLE = \"maze-printable · javascript · muix · anyplot.ai\";\nconst TITLE_HEIGHT = 64;\n\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const chartHeight = height - TITLE_HEIGHT;\n  const margin = 48;\n\n  return (\n    <Box\n      sx={{\n        width,\n        height,\n        bgcolor: t.pageBg,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <Typography\n        sx={{\n          color: t.ink,\n          fontSize: 30,\n          fontWeight: 500,\n          textAlign: \"center\",\n          lineHeight: 1.2,\n          pt: \"16px\",\n          height: TITLE_HEIGHT,\n          fontFamily: \"inherit\",\n        }}\n      >\n        {TITLE}\n      </Typography>\n      <Box\n        sx={{\n          flex: 1,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n        }}\n      >\n        <ScatterChart\n          width={width}\n          height={chartHeight}\n          skipAnimation\n          disableVoronoi\n          series={[\n            { id: \"maze\", type: \"scatter\", data: [{ x: 0, y: 0, id: \"c\" }] },\n          ]}\n          xAxis={[\n            {\n              scaleType: \"linear\",\n              min: 0,\n              max: COLS,\n              disableTicks: true,\n              disableLine: true,\n            },\n          ]}\n          yAxis={[\n            {\n              scaleType: \"linear\",\n              min: 0,\n              max: ROWS,\n              disableTicks: true,\n              disableLine: true,\n            },\n          ]}\n          topAxis={null}\n          bottomAxis={null}\n          leftAxis={null}\n          rightAxis={null}\n          margin={{ top: margin, bottom: margin, left: margin, right: margin }}\n          slots={{ scatter: MazeMark }}\n          slotProps={{ legend: { hidden: true } }}\n        />\n      </Box>\n    </Box>\n  );\n}\n"}