{"spec_id":"maze-circular","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// maze-circular: Circular Maze Puzzle\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-02\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE;\n\n// --- Deterministic PRNG (LCG, no seeded Math.random in the browser) --------\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\n\n// --- Maze topology: concentric rings subdivided into sectors ---------------\n// Ring 0 is a single hub cell at the center; each outer ring roughly doubles\n// its sector count whenever cells would otherwise grow too wide relative to\n// the ring's radial thickness, keeping cells close to square (Theta-maze\n// layout, after Jamis Buck's \"circular maze\" construction).\nconst NUM_RINGS = 7;\nconst rowHeight = 1 / NUM_RINGS;\nconst rows = [[{ i: 0 }]];\nfor (let r = 1; r < NUM_RINGS; r++) {\n  const radius = r / NUM_RINGS;\n  const circumference = 2 * Math.PI * radius;\n  const prevCount = rows[r - 1].length;\n  const cellWidth = circumference / prevCount;\n  const ratio = Math.max(1, Math.round(cellWidth / rowHeight));\n  const cellCount = prevCount * ratio;\n  rows.push(Array.from({ length: cellCount }, (_, i) => ({ i })));\n}\n\nfunction cellKey(r, i) {\n  return r + '-' + i;\n}\nfunction edgeKey(r1, i1, r2, i2) {\n  const a = cellKey(r1, i1);\n  const b = cellKey(r2, i2);\n  return a < b ? a + '|' + b : b + '|' + a;\n}\n\n// Every cell's clockwise and inward neighbor (its counter-clockwise and\n// outward neighbors are the same edges seen from the other side).\nfunction neighbors(r, i) {\n  const list = [];\n  const count = rows[r].length;\n  list.push({ r, i: (i + 1) % count });\n  list.push({ r, i: (i - 1 + count) % count });\n  if (r > 0) {\n    const ratio = count / rows[r - 1].length;\n    list.push({ r: r - 1, i: Math.floor(i / ratio) });\n  }\n  if (r < NUM_RINGS - 1) {\n    const ratio = rows[r + 1].length / count;\n    for (let k = 0; k < ratio; k++) {\n      list.push({ r: r + 1, i: i * ratio + k });\n    }\n  }\n  return list;\n}\n\n// --- Carve a perfect maze: randomized recursive backtracker ----------------\n// A spanning tree over every cell guarantees exactly one path between the\n// entry and the goal, satisfying the \"exactly one solvable path\" contract.\nconst visited = new Set([cellKey(0, 0)]);\nconst passages = new Set();\nconst stack = [{ r: 0, i: 0 }];\nwhile (stack.length) {\n  const cur = stack[stack.length - 1];\n  const open = neighbors(cur.r, cur.i).filter((n) => !visited.has(cellKey(n.r, n.i)));\n  if (open.length === 0) {\n    stack.pop();\n    continue;\n  }\n  const next = open[Math.floor(rand() * open.length)];\n  passages.add(edgeKey(cur.r, cur.i, next.r, next.i));\n  visited.add(cellKey(next.r, next.i));\n  stack.push(next);\n}\n\n// Entry sits on the outer ring at the top; the goal is the center hub.\nconst outerRow = NUM_RINGS - 1;\nconst entryIndex = 0;\n\n// --- Geometry ----------------------------------------------------------------\nconst titleClearance = 90;\nconst cx = size.width / 2;\nconst cy = titleClearance + (size.height - titleClearance) / 2;\nconst maxRadius = Math.min(size.width, size.height - titleClearance) / 2 - 60;\nconst wallWidth = 3.5;\n\nfunction point(radius, angle) {\n  return [cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)];\n}\nfunction cellAngles(r, i) {\n  const count = rows[r].length;\n  const start = (i / count) * 2 * Math.PI - Math.PI / 2;\n  const end = ((i + 1) / count) * 2 * Math.PI - Math.PI / 2;\n  return [start, end];\n}\n// A filled wedge (inner arc, outer arc, two radial edges) for the soft\n// \"first step\" highlight behind the entry cell.\nfunction wedgePath(rInner, rOuter, a0, a1) {\n  const [ix1, iy1] = point(rInner, a0);\n  const [ox1, oy1] = point(rOuter, a0);\n  const [ox2, oy2] = point(rOuter, a1);\n  const [ix2, iy2] = point(rInner, a1);\n  return ['M', ix1, iy1, 'L', ox1, oy1, 'A', rOuter, rOuter, 0, 0, 1, ox2, oy2, 'L', ix2, iy2, 'A', rInner, rInner, 0, 0, 0, ix1, iy1, 'Z'];\n}\n\nconst chart = Highcharts.chart('container', {\n  chart: {\n    backgroundColor: 'transparent',\n    animation: false,\n    style: { fontFamily: 'inherit' },\n    events: {\n      load: function () {\n        const renderer = this.renderer;\n        const wallStyle = { stroke: t.ink, 'stroke-width': wallWidth, fill: 'none', 'stroke-linecap': 'round' };\n        const perimeterStyle = { stroke: t.ink, 'stroke-width': wallWidth + 1.5, fill: 'none', 'stroke-linecap': 'round' };\n\n        // Soft \"first step\" highlight: a faint tint over the entry cell,\n        // hinting at the route into the maze without giving the solution away.\n        const [entryA0, entryA1] = cellAngles(outerRow, entryIndex);\n        const entryInnerRadius = (outerRow / NUM_RINGS) * maxRadius;\n        renderer\n          .path(wedgePath(entryInnerRadius, maxRadius, entryA0, entryA1))\n          .attr({ fill: t.palette[0], 'fill-opacity': 0.1, stroke: 'none' })\n          .add();\n\n        // Ring boundaries: one arc per fine-grained cell, skipped where a\n        // passage (spanning-tree edge) crosses that boundary.\n        for (let r = 1; r < NUM_RINGS; r++) {\n          const radius = (r / NUM_RINGS) * maxRadius;\n          for (let i = 0; i < rows[r].length; i++) {\n            const ratio = rows[r].length / rows[r - 1].length;\n            const parent = Math.floor(i / ratio);\n            if (passages.has(edgeKey(r, i, r - 1, parent))) continue;\n            const [a0, a1] = cellAngles(r, i);\n            const [x1, y1] = point(radius, a0);\n            const [x2, y2] = point(radius, a1);\n            renderer\n              .path(['M', x1, y1, 'A', radius, radius, 0, 0, 1, x2, y2])\n              .attr(wallStyle)\n              .add();\n          }\n        }\n\n        // Outer perimeter: drawn heavier than the interior walls (a classic\n        // maze-print convention that frames the puzzle), with one gap left\n        // open for entry.\n        for (let i = 0; i < rows[outerRow].length; i++) {\n          if (i === entryIndex) continue;\n          const [a0, a1] = cellAngles(outerRow, i);\n          const [x1, y1] = point(maxRadius, a0);\n          const [x2, y2] = point(maxRadius, a1);\n          renderer\n            .path(['M', x1, y1, 'A', maxRadius, maxRadius, 0, 0, 1, x2, y2])\n            .attr(perimeterStyle)\n            .add();\n        }\n\n        // Radial walls: the boundary between a cell and its clockwise\n        // neighbor within the same ring, skipped where a passage crosses.\n        for (let r = 1; r < NUM_RINGS; r++) {\n          const innerR = (r / NUM_RINGS) * maxRadius;\n          const outerR = ((r + 1) / NUM_RINGS) * maxRadius;\n          const count = rows[r].length;\n          for (let i = 0; i < count; i++) {\n            const next = (i + 1) % count;\n            if (passages.has(edgeKey(r, i, r, next))) continue;\n            const angle = (next / count) * 2 * Math.PI - Math.PI / 2;\n            const [x1, y1] = point(innerR, angle);\n            const [x2, y2] = point(outerR, angle);\n            renderer.path(['M', x1, y1, 'L', x2, y2]).attr(wallStyle).add();\n          }\n        }\n\n        // Goal marker at the center hub: a radial gradient (via Highcharts'\n        // native Color/gradient API) gives the hub a subtle raised depth\n        // instead of a flat fill.\n        const goalFill = {\n          radialGradient: { cx: 0.35, cy: 0.35, r: 0.75 },\n          stops: [\n            [0, Highcharts.color(t.palette[1]).brighten(0.35).get()],\n            [1, t.palette[1]],\n          ],\n        };\n        renderer.circle(cx, cy, 16).attr({ fill: goalFill, stroke: t.pageBg, 'stroke-width': 3 }).add();\n        renderer\n          .text('GOAL', cx, cy - 28)\n          .attr({ align: 'center', zIndex: 5 })\n          .css({ color: t.ink, fontSize: '15px', fontWeight: '600' })\n          .add();\n\n        // Entry marker just outside the perimeter gap.\n        const [ea0, ea1] = cellAngles(outerRow, entryIndex);\n        const entryAngle = (ea0 + ea1) / 2;\n        const [ex1, ey1] = point(maxRadius - 4, entryAngle);\n        const [ex2, ey2] = point(maxRadius + 34, entryAngle);\n        renderer\n          .path(['M', ex1, ey1, 'L', ex2, ey2])\n          .attr({ stroke: t.palette[0], 'stroke-width': 5, 'stroke-linecap': 'round' })\n          .add();\n        renderer\n          .text('START', ex2, ey2 - 12)\n          .attr({ align: 'center', zIndex: 5 })\n          .css({ color: t.palette[0], fontSize: '15px', fontWeight: '600' })\n          .add();\n      },\n    },\n  },\n  title: {\n    text: 'maze-circular · javascript · highcharts · anyplot.ai',\n    style: { color: t.ink, fontSize: '27px', fontWeight: '600' },\n  },\n  credits: { enabled: false },\n  series: [],\n});\n"}