Circular Maze Puzzle — Highcharts

A circular maze puzzle visualization featuring concentric rings connected by radial passages. Unlike rectangular mazes, this design creates a unique solving experience where the player navigates inward through ring-shaped corridors. The maze has an entry point on the outer edge and a goal at the center, with algorithmically generated walls ensuring exactly one solvable path.

Circular Maze Puzzle rendered with Highcharts

Renders

JavaScript source (Highcharts)

// anyplot.ai
// maze-circular: Circular Maze Puzzle
// Library: highcharts 12.6.0 | JavaScript 22.23.2
// Quality: 89/100 | Created: 2026-09-02

//# anyplot-orientation: square
const t = window.ANYPLOT_TOKENS;
const size = window.ANYPLOT_SIZE;

// --- Deterministic PRNG (LCG, no seeded Math.random in the browser) --------
let seed = 42;
function rand() {
  seed = (seed * 1664525 + 1013904223) % 4294967296;
  return seed / 4294967296;
}

// --- Maze topology: concentric rings subdivided into sectors ---------------
// Ring 0 is a single hub cell at the center; each outer ring roughly doubles
// its sector count whenever cells would otherwise grow too wide relative to
// the ring's radial thickness, keeping cells close to square (Theta-maze
// layout, after Jamis Buck's "circular maze" construction).
const NUM_RINGS = 7;
const rowHeight = 1 / NUM_RINGS;
const rows = [[{ i: 0 }]];
for (let r = 1; r < NUM_RINGS; r++) {
  const radius = r / NUM_RINGS;
  const circumference = 2 * Math.PI * radius;
  const prevCount = rows[r - 1].length;
  const cellWidth = circumference / prevCount;
  const ratio = Math.max(1, Math.round(cellWidth / rowHeight));
  const cellCount = prevCount * ratio;
  rows.push(Array.from({ length: cellCount }, (_, i) => ({ i })));
}

function cellKey(r, i) {
  return r + '-' + i;
}
function edgeKey(r1, i1, r2, i2) {
  const a = cellKey(r1, i1);
  const b = cellKey(r2, i2);
  return a < b ? a + '|' + b : b + '|' + a;
}

// Every cell's clockwise and inward neighbor (its counter-clockwise and
// outward neighbors are the same edges seen from the other side).
function neighbors(r, i) {
  const list = [];
  const count = rows[r].length;
  list.push({ r, i: (i + 1) % count });
  list.push({ r, i: (i - 1 + count) % count });
  if (r > 0) {
    const ratio = count / rows[r - 1].length;
    list.push({ r: r - 1, i: Math.floor(i / ratio) });
  }
  if (r < NUM_RINGS - 1) {
    const ratio = rows[r + 1].length / count;
    for (let k = 0; k < ratio; k++) {
      list.push({ r: r + 1, i: i * ratio + k });
    }
  }
  return list;
}

// --- Carve a perfect maze: randomized recursive backtracker ----------------
// A spanning tree over every cell guarantees exactly one path between the
// entry and the goal, satisfying the "exactly one solvable path" contract.
const visited = new Set([cellKey(0, 0)]);
const passages = new Set();
const stack = [{ r: 0, i: 0 }];
while (stack.length) {
  const cur = stack[stack.length - 1];
  const open = neighbors(cur.r, cur.i).filter((n) => !visited.has(cellKey(n.r, n.i)));
  if (open.length === 0) {
    stack.pop();
    continue;
  }
  const next = open[Math.floor(rand() * open.length)];
  passages.add(edgeKey(cur.r, cur.i, next.r, next.i));
  visited.add(cellKey(next.r, next.i));
  stack.push(next);
}

// Entry sits on the outer ring at the top; the goal is the center hub.
const outerRow = NUM_RINGS - 1;
const entryIndex = 0;

// --- Geometry ----------------------------------------------------------------
const titleClearance = 90;
const cx = size.width / 2;
const cy = titleClearance + (size.height - titleClearance) / 2;
const maxRadius = Math.min(size.width, size.height - titleClearance) / 2 - 60;
const wallWidth = 3.5;

function point(radius, angle) {
  return [cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)];
}
function cellAngles(r, i) {
  const count = rows[r].length;
  const start = (i / count) * 2 * Math.PI - Math.PI / 2;
  const end = ((i + 1) / count) * 2 * Math.PI - Math.PI / 2;
  return [start, end];
}
// A filled wedge (inner arc, outer arc, two radial edges) for the soft
// "first step" highlight behind the entry cell.
function wedgePath(rInner, rOuter, a0, a1) {
  const [ix1, iy1] = point(rInner, a0);
  const [ox1, oy1] = point(rOuter, a0);
  const [ox2, oy2] = point(rOuter, a1);
  const [ix2, iy2] = point(rInner, a1);
  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'];
}

const chart = Highcharts.chart('container', {
  chart: {
    backgroundColor: 'transparent',
    animation: false,
    style: { fontFamily: 'inherit' },
    events: {
      load: function () {
        const renderer = this.renderer;
        const wallStyle = { stroke: t.ink, 'stroke-width': wallWidth, fill: 'none', 'stroke-linecap': 'round' };
        const perimeterStyle = { stroke: t.ink, 'stroke-width': wallWidth + 1.5, fill: 'none', 'stroke-linecap': 'round' };

        // Soft "first step" highlight: a faint tint over the entry cell,
        // hinting at the route into the maze without giving the solution away.
        const [entryA0, entryA1] = cellAngles(outerRow, entryIndex);
        const entryInnerRadius = (outerRow / NUM_RINGS) * maxRadius;
        renderer
          .path(wedgePath(entryInnerRadius, maxRadius, entryA0, entryA1))
          .attr({ fill: t.palette[0], 'fill-opacity': 0.1, stroke: 'none' })
          .add();

        // Ring boundaries: one arc per fine-grained cell, skipped where a
        // passage (spanning-tree edge) crosses that boundary.
        for (let r = 1; r < NUM_RINGS; r++) {
          const radius = (r / NUM_RINGS) * maxRadius;
          for (let i = 0; i < rows[r].length; i++) {
            const ratio = rows[r].length / rows[r - 1].length;
            const parent = Math.floor(i / ratio);
            if (passages.has(edgeKey(r, i, r - 1, parent))) continue;
            const [a0, a1] = cellAngles(r, i);
            const [x1, y1] = point(radius, a0);
            const [x2, y2] = point(radius, a1);
            renderer
              .path(['M', x1, y1, 'A', radius, radius, 0, 0, 1, x2, y2])
              .attr(wallStyle)
              .add();
          }
        }

        // Outer perimeter: drawn heavier than the interior walls (a classic
        // maze-print convention that frames the puzzle), with one gap left
        // open for entry.
        for (let i = 0; i < rows[outerRow].length; i++) {
          if (i === entryIndex) continue;
          const [a0, a1] = cellAngles(outerRow, i);
          const [x1, y1] = point(maxRadius, a0);
          const [x2, y2] = point(maxRadius, a1);
          renderer
            .path(['M', x1, y1, 'A', maxRadius, maxRadius, 0, 0, 1, x2, y2])
            .attr(perimeterStyle)
            .add();
        }

        // Radial walls: the boundary between a cell and its clockwise
        // neighbor within the same ring, skipped where a passage crosses.
        for (let r = 1; r < NUM_RINGS; r++) {
          const innerR = (r / NUM_RINGS) * maxRadius;
          const outerR = ((r + 1) / NUM_RINGS) * maxRadius;
          const count = rows[r].length;
          for (let i = 0; i < count; i++) {
            const next = (i + 1) % count;
            if (passages.has(edgeKey(r, i, r, next))) continue;
            const angle = (next / count) * 2 * Math.PI - Math.PI / 2;
            const [x1, y1] = point(innerR, angle);
            const [x2, y2] = point(outerR, angle);
            renderer.path(['M', x1, y1, 'L', x2, y2]).attr(wallStyle).add();
          }
        }

        // Goal marker at the center hub: a radial gradient (via Highcharts'
        // native Color/gradient API) gives the hub a subtle raised depth
        // instead of a flat fill.
        const goalFill = {
          radialGradient: { cx: 0.35, cy: 0.35, r: 0.75 },
          stops: [
            [0, Highcharts.color(t.palette[1]).brighten(0.35).get()],
            [1, t.palette[1]],
          ],
        };
        renderer.circle(cx, cy, 16).attr({ fill: goalFill, stroke: t.pageBg, 'stroke-width': 3 }).add();
        renderer
          .text('GOAL', cx, cy - 28)
          .attr({ align: 'center', zIndex: 5 })
          .css({ color: t.ink, fontSize: '15px', fontWeight: '600' })
          .add();

        // Entry marker just outside the perimeter gap.
        const [ea0, ea1] = cellAngles(outerRow, entryIndex);
        const entryAngle = (ea0 + ea1) / 2;
        const [ex1, ey1] = point(maxRadius - 4, entryAngle);
        const [ex2, ey2] = point(maxRadius + 34, entryAngle);
        renderer
          .path(['M', ex1, ey1, 'L', ex2, ey2])
          .attr({ stroke: t.palette[0], 'stroke-width': 5, 'stroke-linecap': 'round' })
          .add();
        renderer
          .text('START', ex2, ey2 - 12)
          .attr({ align: 'center', zIndex: 5 })
          .css({ color: t.palette[0], fontSize: '15px', fontWeight: '600' })
          .add();
      },
    },
  },
  title: {
    text: 'maze-circular · javascript · highcharts · anyplot.ai',
    style: { color: t.ink, fontSize: '27px', fontWeight: '600' },
  },
  credits: { enabled: false },
  series: [],
});

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/maze-circular/highcharts/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "maze-circular",
  "language": "javascript",
  "library": "highcharts",
  "page": "https://anyplot.ai/maze-circular/javascript/highcharts",
  "hub": "https://anyplot.ai/maze-circular",
  "code_json": "https://api.anyplot.ai/specs/maze-circular/highcharts/code",
  "spec_json": "https://api.anyplot.ai/specs/maze-circular",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/maze-circular/javascript/highcharts/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/maze-circular/javascript/highcharts/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/maze-circular/javascript/highcharts/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/maze-circular/javascript/highcharts/plot-dark.html",
  "quality_score": 89.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Circular Maze Puzzle on anyplot.ai.

Other implementations