{"spec_id":"maze-printable","library":"d3","language":"javascript","code":"// anyplot.ai\n// maze-printable: Printable Maze Puzzle\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-05\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Deterministic PRNG (mulberry32) — browser has no seeded Math.random ----\nconst mulberry32 = (seed) => () => {\n  seed |= 0;\n  seed = (seed + 0x6d2b79f5) | 0;\n  let z = Math.imul(seed ^ (seed >>> 15), 1 | seed);\n  z = (z + Math.imul(z ^ (z >>> 7), 61 | z)) ^ z;\n  return ((z ^ (z >>> 14)) >>> 0) / 4294967296;\n};\nconst rng = mulberry32(20260905);\n\n// --- Maze generation: randomized depth-first search (recursive backtracker) -\n// Guarantees a \"perfect maze\" — exactly one path between any two cells.\nconst cols = 22;\nconst rows = 22;\nconst cells = Array.from({ length: cols * rows }, () => ({\n  top: true,\n  right: true,\n  bottom: true,\n  left: true,\n  visited: false,\n}));\nconst index = (col, row) => row * cols + col;\n\nconst stack = [{ col: 0, row: 0 }];\ncells[index(0, 0)].visited = true;\nwhile (stack.length > 0) {\n  const { col, row } = stack[stack.length - 1];\n  const candidates = [\n    { col, row: row - 1, dir: \"top\", opp: \"bottom\" },\n    { col: col + 1, row, dir: \"right\", opp: \"left\" },\n    { col, row: row + 1, dir: \"bottom\", opp: \"top\" },\n    { col: col - 1, row, dir: \"left\", opp: \"right\" },\n  ].filter(\n    (n) =>\n      n.col >= 0 &&\n      n.col < cols &&\n      n.row >= 0 &&\n      n.row < rows &&\n      !cells[index(n.col, n.row)].visited,\n  );\n\n  if (candidates.length === 0) {\n    stack.pop();\n    continue;\n  }\n  const next = candidates[Math.floor(rng() * candidates.length)];\n  const current = cells[index(col, row)];\n  const neighbor = cells[index(next.col, next.row)];\n  current[next.dir] = false;\n  neighbor[next.opp] = false;\n  neighbor.visited = true;\n  stack.push({ col: next.col, row: next.row });\n}\n\n// --- Layout -------------------------------------------------------------\nconst margin = { top: 120, right: 70, bottom: 80, left: 70 };\nconst availableWidth = width - margin.left - margin.right;\nconst availableHeight = height - margin.top - margin.bottom;\nconst cellSize = Math.floor(\n  Math.min(availableWidth / cols, availableHeight / rows),\n);\nconst mazeWidth = cellSize * cols;\nconst mazeHeight = cellSize * rows;\nconst offsetX = margin.left + (availableWidth - mazeWidth) / 2;\nconst offsetY = margin.top + (availableHeight - mazeHeight) / 2;\n\n// --- SVG mount ------------------------------------------------------------\nconst svg = d3\n  .select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${offsetX},${offsetY})`);\n\n// --- Walls: merge same-direction adjacent segments into fewer path elements\n// via d3.path, and give the outer border a heavier stroke than the interior\n// walls so the puzzle frame reads as a clear visual hierarchy (DE-01) --------\nconst innerWallPaths = [];\n\n// Interior horizontal grid lines (between row r-1 and row r), run-length\n// encoded with d3.path so a contiguous stretch of wall becomes one subpath.\nd3.range(1, rows).forEach((r) => {\n  const p = d3.path();\n  let runStart = null;\n  d3.range(cols + 1).forEach((col) => {\n    const hasWall = col < cols && cells[index(col, r - 1)].bottom;\n    if (hasWall && runStart === null) runStart = col;\n    if (!hasWall && runStart !== null) {\n      p.moveTo(runStart * cellSize, r * cellSize);\n      p.lineTo(col * cellSize, r * cellSize);\n      runStart = null;\n    }\n  });\n  const d = p.toString();\n  if (d) innerWallPaths.push(d);\n});\n\n// Interior vertical grid lines (between col c-1 and col c), same run-length\n// merge along the column.\nd3.range(1, cols).forEach((c) => {\n  const p = d3.path();\n  let runStart = null;\n  d3.range(rows + 1).forEach((row) => {\n    const hasWall = row < rows && cells[index(c - 1, row)].right;\n    if (hasWall && runStart === null) runStart = row;\n    if (!hasWall && runStart !== null) {\n      p.moveTo(c * cellSize, runStart * cellSize);\n      p.lineTo(c * cellSize, row * cellSize);\n      runStart = null;\n    }\n  });\n  const d = p.toString();\n  if (d) innerWallPaths.push(d);\n});\n\ng.selectAll(\"path.wall-inner\")\n  .data(innerWallPaths)\n  .join(\"path\")\n  .attr(\"class\", \"wall-inner\")\n  .attr(\"d\", (d) => d)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 3)\n  .attr(\"stroke-linecap\", \"square\");\n\n// Outer border: the maze boundary is always fully closed (the DFS carver\n// never removes a perimeter wall), so it renders as a single heavier-stroke\n// rectangle that frames the puzzle.\nconst borderPath = d3.path();\nborderPath.moveTo(0, 0);\nborderPath.lineTo(mazeWidth, 0);\nborderPath.lineTo(mazeWidth, mazeHeight);\nborderPath.lineTo(0, mazeHeight);\nborderPath.closePath();\n\ng.append(\"path\")\n  .attr(\"class\", \"wall-border\")\n  .attr(\"d\", borderPath.toString())\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 6)\n  .attr(\"stroke-linejoin\", \"miter\");\n\n// --- Start / goal markers ---------------------------------------------------\nconst markers = [\n  { col: 0, row: 0, label: \"S\", name: \"Start\", color: t.palette[0] },\n  {\n    col: cols - 1,\n    row: rows - 1,\n    label: \"G\",\n    name: \"Goal\",\n    color: t.palette[1],\n  },\n];\n\nconst markerGroup = g\n  .selectAll(\"g.marker\")\n  .data(markers)\n  .join(\"g\")\n  .attr(\"class\", \"marker\")\n  .attr(\n    \"transform\",\n    (d) =>\n      `translate(${d.col * cellSize + cellSize / 2},${d.row * cellSize + cellSize / 2})`,\n  );\n\nmarkerGroup\n  .append(\"circle\")\n  .attr(\"r\", cellSize * 0.36)\n  .attr(\"fill\", (d) => d.color);\nmarkerGroup\n  .append(\"text\")\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"central\")\n  .attr(\"fill\", t.pageBg)\n  .style(\"font-size\", `${Math.round(cellSize * 0.42)}px`)\n  .style(\"font-weight\", \"700\")\n  .text((d) => d.label);\n\n// --- Title ------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"maze-printable · javascript · d3 · anyplot.ai\");\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 88)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .text(`${cols} × ${rows} grid · single guaranteed solution path`);\n\n// --- Legend -----------------------------------------------------------------\nconst legend = svg\n  .append(\"g\")\n  .attr(\n    \"transform\",\n    `translate(${width / 2 - 130},${height - margin.bottom / 2 + 8})`,\n  );\n\nconst legendItems = legend\n  .selectAll(\"g.legend-item\")\n  .data(markers)\n  .join(\"g\")\n  .attr(\"class\", \"legend-item\")\n  .attr(\"transform\", (_, i) => `translate(${i * 140},0)`);\n\nlegendItems\n  .append(\"circle\")\n  .attr(\"r\", 12)\n  .attr(\"cy\", -6)\n  .attr(\"fill\", (d) => d.color);\nlegendItems\n  .append(\"text\")\n  .attr(\"x\", 22)\n  .attr(\"y\", -1)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .text((d) => d.name);\n"}