{"spec_id":"maze-circular","library":"muix","language":"javascript","code":"// anyplot.ai\n// maze-circular: Circular Maze Puzzle\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 87/100 | Created: 2026-09-02\n//# anyplot-orientation: square\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// print-friendly output\" per spec — so its ink/paper colors stay fixed across\n// themes, like the crossword-basic muix implementation does for its grid.\n// 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 hub cell + RINGS concentric rings of SECTORS cells ----\nconst RINGS = 7;\nconst SECTORS = 12;\nconst ENTRY_SECTOR = 0;\n\n// Spec's difficulty parameter — \"7 rings with medium difficulty\" is the\n// spec's own example, so that's the tier demonstrated here. Difficulty biases\n// how the spanning tree grows: a high CONTINUE_BIAS keeps extending the most\n// recently carved passage (long, winding corridors with few branch points —\n// harder to trace by eye), while a low bias favors a uniformly random\n// frontier pick (many short branches, more decision points — easier). The\n// carve is still a spanning tree either way, so \"exactly one solution\" always\n// holds; only the corridor character (the spec's \"passage density\" feel)\n// changes with difficulty.\nconst DIFFICULTY = \"medium\";\nconst CONTINUE_BIAS = { easy: 0.15, medium: 0.55, hard: 0.85 }[DIFFICULTY];\n\nconst adjacency = new Map();\nfunction addEdge(a, b) {\n  const key = a < b ? `${a}|${b}` : `${b}|${a}`;\n  if (!adjacency.has(a)) adjacency.set(a, []);\n  if (!adjacency.has(b)) adjacency.set(b, []);\n  adjacency.get(a).push({ to: b, key });\n  adjacency.get(b).push({ to: a, key });\n}\n\nfor (let s = 0; s < SECTORS; s++) addEdge(\"hub\", `1-${s}`);\nfor (let r = 1; r <= RINGS; r++) {\n  for (let s = 0; s < SECTORS; s++) addEdge(`${r}-${s}`, `${r}-${(s + 1) % SECTORS}`);\n}\nfor (let r = 1; r < RINGS; r++) {\n  for (let s = 0; s < SECTORS; s++) addEdge(`${r}-${s}`, `${r + 1}-${s}`);\n}\n\n// Randomized Prim's algorithm, biased by difficulty, carves a spanning tree\n// over the cell graph. A spanning tree has no cycles, so there is exactly one\n// path between the hub and any cell — including the entry — which is what\n// \"exactly one solvable path\" requires, regardless of the bias.\nconst passageKeys = new Set();\nconst visited = new Set([\"hub\"]);\nlet frontier = adjacency.get(\"hub\").map((edge) => ({ ...edge, from: \"hub\" }));\nlet lastAdded = \"hub\";\nwhile (frontier.length > 0) {\n  const continuing = rand() < CONTINUE_BIAS ? frontier.filter((edge) => edge.from === lastAdded) : [];\n  const pool = continuing.length > 0 ? continuing : frontier;\n  const edge = pool[Math.floor(rand() * pool.length)];\n  frontier.splice(frontier.indexOf(edge), 1);\n  if (visited.has(edge.to)) continue;\n  visited.add(edge.to);\n  lastAdded = edge.to;\n  passageKeys.add(edge.key);\n  adjacency.get(edge.to).forEach((next) => {\n    if (!visited.has(next.to)) frontier.push({ ...next, from: edge.to });\n  });\n}\n\nconst isPassage = (a, b) => passageKeys.has(a < b ? `${a}|${b}` : `${b}|${a}`);\n\n// --- Polar geometry (unit disc, radius 1 = outer wall) ----------------------\nconst HUB_R = 1 / (RINGS + 1);\nconst RING_WIDTH = (1 - HUB_R) / RINGS;\nconst ringInner = (r) => HUB_R + (r - 1) * RING_WIDTH;\nconst ringOuter = (r) => HUB_R + r * RING_WIDTH;\nconst ANGLE_STEP = (2 * Math.PI) / SECTORS;\nconst angleOf = (s) => -Math.PI / 2 + s * ANGLE_STEP; // sector 0 starts at 12 o'clock\n\nfunction polar(cx, cy, scale, radius, angle) {\n  return [cx + scale * radius * Math.cos(angle), cy + scale * radius * Math.sin(angle)];\n}\n\nfunction arcPath(cx, cy, scale, radius, a0, a1) {\n  const [x0, y0] = polar(cx, cy, scale, radius, a0);\n  const [x1, y1] = polar(cx, cy, scale, radius, a1);\n  const large = a1 - a0 > Math.PI ? 1 : 0;\n  return `M ${x0} ${y0} A ${scale * radius} ${scale * radius} 0 ${large} 1 ${x1} ${y1}`;\n}\n\n// Draws the whole puzzle as raw SVG paths sized off the chart's own drawing\n// area — MUI X owns layout/scaling, the maze geometry is ours.\nfunction MazeMark() {\n  const { left, top, width, height } = useDrawingArea();\n  const cx = left + width / 2;\n  const cy = top + height / 2;\n  const halfDim = Math.min(width, height) / 2;\n  const scale = halfDim * 0.8;\n\n  const walls = [];\n\n  // Hub <-> ring 1 boundary\n  for (let s = 0; s < SECTORS; s++) {\n    if (!isPassage(\"hub\", `1-${s}`)) {\n      walls.push(arcPath(cx, cy, scale, HUB_R, angleOf(s), angleOf(s + 1)));\n    }\n  }\n\n  // Sector-divider (circumferential) walls, per ring\n  for (let r = 1; r <= RINGS; r++) {\n    for (let s = 0; s < SECTORS; s++) {\n      const a = `${r}-${s}`;\n      const b = `${r}-${(s + 1) % SECTORS}`;\n      if (isPassage(a, b)) continue;\n      const boundaryAngle = angleOf(s + 1);\n      const [x0, y0] = polar(cx, cy, scale, ringInner(r), boundaryAngle);\n      const [x1, y1] = polar(cx, cy, scale, ringOuter(r), boundaryAngle);\n      walls.push(`M ${x0} ${y0} L ${x1} ${y1}`);\n    }\n  }\n\n  // Ring-boundary (radial) walls between adjacent rings\n  for (let r = 1; r < RINGS; r++) {\n    for (let s = 0; s < SECTORS; s++) {\n      if (!isPassage(`${r}-${s}`, `${r + 1}-${s}`)) {\n        walls.push(arcPath(cx, cy, scale, ringOuter(r), angleOf(s), angleOf(s + 1)));\n      }\n    }\n  }\n\n  // Outer perimeter, with a gap left open at the entry sector\n  for (let s = 0; s < SECTORS; s++) {\n    if (s === ENTRY_SECTOR) continue;\n    walls.push(arcPath(cx, cy, scale, 1, angleOf(s), angleOf(s + 1)));\n  }\n\n  const entryMid = angleOf(ENTRY_SECTOR) + ANGLE_STEP / 2;\n  const [tickX0, tickY0] = polar(cx, cy, scale, 1, entryMid);\n  const [tickX1, tickY1] = polar(cx, cy, scale, 1.06, entryMid);\n  const [labelX, labelY] = polar(cx, cy, scale, 1.16, entryMid);\n\n  return (\n    <g>\n      <rect\n        x={left}\n        y={top}\n        width={width}\n        height={height}\n        rx={20}\n        ry={20}\n        fill={PAPER}\n        stroke={INK}\n        strokeWidth={1.5}\n      />\n      {walls.map((d, i) => (\n        <path key={i} d={d} fill=\"none\" stroke={INK} strokeWidth={3.5} strokeLinecap=\"round\" />\n      ))}\n      <line x1={tickX0} y1={tickY0} x2={tickX1} y2={tickY1} stroke={INK} strokeWidth={3.5} strokeLinecap=\"round\" />\n      <text x={labelX} y={labelY} fontSize={20} fontWeight={600} fill={INK} textAnchor=\"middle\" dominantBaseline=\"middle\">\n        START\n      </text>\n      <circle cx={cx} cy={cy} r={scale * HUB_R * 0.6} fill={GOAL_GREEN} stroke={PAPER} strokeWidth={2} />\n      <text x={left + 16} y={top + height - 16} fontSize={14} fill={INK} fillOpacity={0.65} textAnchor=\"start\">\n        {RINGS} rings · {DIFFICULTY} difficulty\n      </text>\n    </g>\n  );\n}\n\nconst TITLE = \"maze-circular · 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 sx={{ width, height, bgcolor: t.pageBg, display: \"flex\", flexDirection: \"column\" }}>\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 sx={{ flex: 1, display: \"flex\", alignItems: \"center\", justifyContent: \"center\" }}>\n        <ScatterChart\n          width={width}\n          height={chartHeight}\n          skipAnimation\n          disableVoronoi\n          series={[{ id: \"maze\", type: \"scatter\", data: [{ x: 0, y: 0, id: \"c\" }] }]}\n          xAxis={[{ scaleType: \"linear\", min: -1.3, max: 1.3, disableTicks: true, disableLine: true }]}\n          yAxis={[{ scaleType: \"linear\", min: -1.3, max: 1.3, disableTicks: true, disableLine: true }]}\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"}