{"spec_id":"maze-circular","library":"d3","language":"javascript","code":"// anyplot.ai\n// maze-circular: Circular Maze Puzzle\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-02\n\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Deterministic PRNG (LCG, seed=42) --------------------------------------\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 16807) % 2147483647;\n  return (seed - 1) / 2147483646;\n}\nfunction randInt(n) {\n  return Math.floor(rand() * n);\n}\n\n// --- Polar grid: concentric rings, each subdivided into sectors ------------\n// Row 0 is the single central cell (the goal). Each outward row roughly\n// doubles its sector count whenever the arc length per cell would otherwise\n// grow past the radial ring height, keeping cells close to square.\nconst numRings = 8;\nconst difficulty = \"medium\"; // \"easy\" | \"medium\" | \"hard\"\n\nconst rows = [[{ r: 0, i: 0 }]];\nfor (let r = 1; r < numRings; r++) {\n  const radius = r / numRings;\n  const circumference = 2 * Math.PI * radius;\n  const prevCount = rows[r - 1].length;\n  const cellWidth = circumference / prevCount;\n  const rowHeight = 1 / numRings;\n  const ratio = Math.max(1, Math.round(cellWidth / rowHeight));\n  const cellCount = prevCount * ratio;\n  rows.push(Array.from({ length: cellCount }, (_, i) => ({ r, i })));\n}\n\nfunction parentIndex(r, i) {\n  return Math.floor((i * rows[r - 1].length) / rows[r].length);\n}\n\nconst childrenMap = []; // childrenMap[r][parentIndex] -> [childIndex, ...]\nfor (let r = 0; r < numRings - 1; r++) {\n  const map = Array.from({ length: rows[r].length }, () => []);\n  rows[r + 1].forEach((_, i) => map[parentIndex(r + 1, i)].push(i));\n  childrenMap.push(map);\n}\n\nfunction cellKey(c) {\n  return `${c.r},${c.i}`;\n}\nfunction edgeKey(a, b) {\n  const ka = cellKey(a);\n  const kb = cellKey(b);\n  return ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;\n}\nfunction getNeighbors(c) {\n  const count = rows[c.r].length;\n  const neighbors = [];\n  if (count > 1) {\n    neighbors.push({ r: c.r, i: (c.i + 1) % count });\n    neighbors.push({ r: c.r, i: (c.i - 1 + count) % count });\n  }\n  if (c.r > 0) neighbors.push({ r: c.r - 1, i: parentIndex(c.r, c.i) });\n  if (c.r < numRings - 1) {\n    for (const child of childrenMap[c.r][c.i]) neighbors.push({ r: c.r + 1, i: child });\n  }\n  return neighbors;\n}\n\n// --- Maze carving: growing tree over the spanning graph ---------------------\n// Always linking to an unvisited cell keeps the result a spanning tree, which\n// guarantees exactly one path between the center and any other cell.\nconst linked = new Set();\nfunction link(a, b) {\n  linked.add(edgeKey(a, b));\n}\nfunction isLinked(a, b) {\n  return linked.has(edgeKey(a, b));\n}\n\nfunction pickFrontierIndex(n) {\n  if (difficulty === \"hard\") return n - 1; // recursive backtracker: long winding corridors\n  if (difficulty === \"easy\") return randInt(n); // Prim's-style: short, branchy dead ends\n  return rand() < 0.5 ? n - 1 : randInt(n); // medium: blend of both\n}\n\nconst visited = new Set([cellKey({ r: 0, i: 0 })]);\nconst frontier = [{ r: 0, i: 0 }];\nwhile (frontier.length) {\n  const idx = pickFrontierIndex(frontier.length);\n  const current = frontier[idx];\n  const candidates = getNeighbors(current).filter((n) => !visited.has(cellKey(n)));\n  if (candidates.length === 0) {\n    frontier.splice(idx, 1);\n    continue;\n  }\n  const next = candidates[randInt(candidates.length)];\n  link(current, next);\n  visited.add(cellKey(next));\n  frontier.push(next);\n}\n\nconst outerRow = numRings - 1;\nconst entryIndex = randInt(rows[outerRow].length);\n\n// --- Geometry -----------------------------------------------------------\nconst outerRadius = 500;\nconst cx = width / 2;\nconst cy = 620;\n\nfunction polarPoint(radius, angle) {\n  return [Math.sin(angle) * radius, -Math.cos(angle) * radius];\n}\nfunction radialWallPath(r0, r1, angle) {\n  const [x0, y0] = polarPoint(r0, angle);\n  const [x1, y1] = polarPoint(r1, angle);\n  return `M${x0},${y0}L${x1},${y1}`;\n}\nconst arcGen = d3.arc();\nfunction ringWallPath(radius, a0, a1) {\n  return arcGen({ innerRadius: radius, outerRadius: radius, startAngle: a0, endAngle: a1 });\n}\n\n// --- Wall descriptors: collect first, bind with .data().join() -------------\nconst wallData = [];\nfor (let r = 0; r < numRings; r++) {\n  const count = rows[r].length;\n  const angleStep = (2 * Math.PI) / count;\n  const rInner = (r / numRings) * outerRadius;\n  const rOuter = ((r + 1) / numRings) * outerRadius;\n\n  for (let i = 0; i < count; i++) {\n    // Radial wall between cell i and its clockwise neighbor\n    if (count > 1) {\n      const cw = { r, i: (i + 1) % count };\n      if (!isLinked({ r, i }, cw)) {\n        wallData.push({ d: radialWallPath(rInner, rOuter, (i + 1) * angleStep) });\n      }\n    }\n\n    // Outward wall(s): true outer boundary, or the boundary with row r+1\n    if (r === outerRow) {\n      if (i !== entryIndex) {\n        wallData.push({ d: ringWallPath(rOuter, i * angleStep, (i + 1) * angleStep) });\n      }\n    } else {\n      const childCount = rows[r + 1].length;\n      const childAngleStep = (2 * Math.PI) / childCount;\n      for (const child of childrenMap[r][i]) {\n        if (!isLinked({ r, i }, { r: r + 1, i: child })) {\n          wallData.push({ d: ringWallPath(rOuter, child * childAngleStep, (child + 1) * childAngleStep) });\n        }\n      }\n    }\n  }\n}\n\n// --- SVG mount ----------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${cx},${cy})`);\nconst walls = g.append(\"g\").attr(\"fill\", \"none\").attr(\"stroke\", t.ink).attr(\"stroke-width\", 4).attr(\"stroke-linecap\", \"round\");\n\nwalls\n  .selectAll(\"path\")\n  .data(wallData)\n  .join(\"path\")\n  .attr(\"d\", (d) => d.d);\n\n// --- Goal marker (center) ------------------------------------------------\ng.append(\"circle\").attr(\"r\", 16).attr(\"fill\", t.palette[0]);\n\n// --- Start marker (outer edge gap): bold inward-pointing arrow -------------\n// Sized to match the goal marker's visual weight so it still reads at\n// mobile-thumbnail scale, per review feedback (the old thin tick vanished).\nconst entryAngleStep = (2 * Math.PI) / rows[outerRow].length;\nconst entryAngle = (entryIndex + 0.5) * entryAngleStep;\nconst arrowBaseRadius = outerRadius + 40;\nconst arrowTipRadius = outerRadius - 4;\nconst arrowHalfWidthAngle = 18 / arrowBaseRadius;\nconst [tipX, tipY] = polarPoint(arrowTipRadius, entryAngle);\nconst [baseX0, baseY0] = polarPoint(arrowBaseRadius, entryAngle - arrowHalfWidthAngle);\nconst [baseX1, baseY1] = polarPoint(arrowBaseRadius, entryAngle + arrowHalfWidthAngle);\ng.append(\"path\")\n  .attr(\"d\", `M${tipX},${tipY}L${baseX0},${baseY0}L${baseX1},${baseY1}Z`)\n  .attr(\"fill\", t.palette[0]);\n\n// --- Caption -------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", cx)\n  .attr(\"y\", 1165)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"17px\")\n  .text(`${numRings - 1} rings · ${difficulty} difficulty · green marks the goal (center) and start (outer edge)`);\n\n// --- Title -----------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 50)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"maze-circular · javascript · d3 · anyplot.ai\");\n"}