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.

""" anyplot.ai
maze-circular: Circular Maze Puzzle
Library: seaborn 0.13.2 | Python 3.13.13
Quality: 84/100 | Updated: 2026-05-20
"""
import collections
import os
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
BRAND = "#009E73" # Okabe-Ito pos 1 — entry
GOAL_COLOR = "#AE3030" # Okabe-Ito pos 5 — goal star
PATH_COLOR = "#2ABCCD" # Okabe-Ito pos 6 — solution path
sns.set_theme(
style="white",
rc={
"figure.facecolor": PAGE_BG,
"axes.facecolor": PAGE_BG,
"text.color": INK,
"legend.facecolor": ELEVATED_BG,
"legend.edgecolor": INK_SOFT,
},
)
# Maze parameters
np.random.seed(42)
RINGS = 7
SECTORS = 12
SECTOR_ANGLE = 2 * np.pi / SECTORS
TOTAL_CELLS = 1 + RINGS * SECTORS # center (0) + ring cells
def cell_id(r, s):
"""Ring r (1..RINGS) sector s → cell index. Center = 0."""
return 1 + (r - 1) * SECTORS + s
# Geometric constants
INNER_R = 0.12
RING_W = (0.82 - INNER_R) / RINGS
OUTER_R = INNER_R + RINGS * RING_W
FRAME_R = OUTER_R + 0.04
WALL_LW = 2.2
# Union-Find with path compression
parent = list(range(TOTAL_CELLS))
uf_rank = [0] * TOTAL_CELLS
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(a, b):
ra, rb = find(a), find(b)
if ra == rb:
return False
if uf_rank[ra] < uf_rank[rb]:
ra, rb = rb, ra
parent[rb] = ra
if uf_rank[ra] == uf_rank[rb]:
uf_rank[ra] += 1
return True
# Build walls: radial (ring-to-ring) and circular (sector-to-sector within ring)
walls = []
for r in range(RINGS):
for s in range(SECTORS):
c1 = 0 if r == 0 else cell_id(r, s)
c2 = cell_id(r + 1, s)
walls.append(("radial", r, s, c1, c2))
for r in range(1, RINGS + 1):
for s in range(SECTORS):
c1 = cell_id(r, s)
c2 = cell_id(r, (s + 1) % SECTORS)
walls.append(("circular", r, s, c1, c2))
np.random.shuffle(walls)
# Kruskal's spanning tree — guarantees exactly one solution
passages = set()
adjacency = collections.defaultdict(set)
for wall in walls:
wtype, r, s, c1, c2 = wall
if union(c1, c2):
passages.add((wtype, r, s))
adjacency[c1].add(c2)
adjacency[c2].add(c1)
# BFS from entry cell to center for solution path
ENTRY_SECTOR = 0
entry_cell = cell_id(RINGS, ENTRY_SECTOR)
goal_cell = 0
bfs_q = collections.deque([entry_cell])
prev = {entry_cell: None}
while bfs_q:
curr = bfs_q.popleft()
if curr == goal_cell:
break
for nxt in sorted(adjacency[curr]):
if nxt not in prev:
prev[nxt] = curr
bfs_q.append(nxt)
solution_path = []
node = goal_cell
while node is not None:
solution_path.append(node)
node = prev.get(node)
solution_path.reverse()
def cell_center(cell):
"""(x, y) of cell center in normalized coordinates."""
if cell == 0:
return 0.0, 0.0
idx = cell - 1
r = idx // SECTORS + 1
s = idx % SECTORS
radius = INNER_R + (r - 0.5) * RING_W
angle = (s + 0.5) * SECTOR_ANGLE
return radius * np.cos(angle), radius * np.sin(angle)
# ---- Drawing ----
fig, ax = plt.subplots(figsize=(6, 6), dpi=400, facecolor=PAGE_BG)
ax.set_facecolor(PAGE_BG)
ax.set_aspect("equal")
# Center zone
center_circle = mpatches.Circle((0, 0), INNER_R, facecolor=ELEVATED_BG, edgecolor=INK, linewidth=WALL_LW, zorder=2)
ax.add_patch(center_circle)
# Circular arc walls at each ring boundary r=1..RINGS (smooth vector arcs)
for r in range(1, RINGS + 1):
radius = INNER_R + r * RING_W
for s in range(SECTORS):
if ("circular", r, s) not in passages:
t1 = np.degrees(s * SECTOR_ANGLE)
t2 = np.degrees((s + 1) * SECTOR_ANGLE)
arc = mpatches.Arc(
(0, 0), 2 * radius, 2 * radius, theta1=t1, theta2=t2, color=INK, linewidth=WALL_LW, zorder=3
)
ax.add_patch(arc)
# Outer boundary frame with entry gap
entry_angle_mid = (ENTRY_SECTOR + 0.5) * SECTOR_ANGLE
gap_half_rad = SECTOR_ANGLE * 0.32
gap_start_deg = np.degrees(entry_angle_mid - gap_half_rad)
gap_end_deg = np.degrees(entry_angle_mid + gap_half_rad)
frame_arc = mpatches.Arc(
(0, 0),
2 * FRAME_R,
2 * FRAME_R,
theta1=gap_end_deg,
theta2=gap_start_deg + 360,
color=INK,
linewidth=WALL_LW + 0.5,
zorder=4,
)
ax.add_patch(frame_arc)
# Radial walls (smooth vector line segments)
for r in range(RINGS):
r_inner = INNER_R + r * RING_W
r_outer = INNER_R + (r + 1) * RING_W
for s in range(SECTORS):
if ("radial", r, s) not in passages:
angle = s * SECTOR_ANGLE
x1, y1 = r_inner * np.cos(angle), r_inner * np.sin(angle)
x2, y2 = r_outer * np.cos(angle), r_outer * np.sin(angle)
ax.plot([x1, x2], [y1, y2], color=INK, linewidth=WALL_LW, zorder=3, solid_capstyle="round")
# Solution path — seaborn lineplot traces BFS solution through cell centers
px = [cell_center(c)[0] for c in solution_path]
py = [cell_center(c)[1] for c in solution_path]
path_df = pd.DataFrame({"x": px, "y": py})
sns.lineplot(
data=path_df,
x="x",
y="y",
ax=ax,
color=PATH_COLOR,
linewidth=2.2,
alpha=0.65,
zorder=2,
label="Solution Path",
sort=False,
estimator=None,
)
# Entry marker — seaborn scatterplot
ex = (FRAME_R + 0.07) * np.cos(entry_angle_mid)
ey = (FRAME_R + 0.07) * np.sin(entry_angle_mid)
entry_df = pd.DataFrame({"x": [ex], "y": [ey]})
sns.scatterplot(data=entry_df, x="x", y="y", ax=ax, color=BRAND, s=120, zorder=7, label="Entry Point")
ax.annotate(
"START",
xy=(ex, ey),
xytext=(ex + 0.17, ey + 0.04),
fontsize=8,
fontweight="bold",
ha="left",
va="center",
color=BRAND,
bbox={"boxstyle": "round,pad=0.3", "facecolor": ELEVATED_BG, "edgecolor": BRAND, "alpha": 0.9},
arrowprops={"arrowstyle": "-", "color": BRAND, "lw": 0.8},
zorder=7,
)
# Goal star at center
ax.text(0, 0, "★", fontsize=16, ha="center", va="center", color=GOAL_COLOR, fontweight="bold", zorder=5)
# Legend — combine seaborn-generated handles with manual goal entry
goal_handle = plt.Line2D(
[0], [0], marker="*", color="w", markerfacecolor=GOAL_COLOR, markersize=10, label="Goal (Center)"
)
handles, labels = ax.get_legend_handles_labels()
handles.append(goal_handle)
labels.append("Goal (Center)")
leg = ax.legend(
handles=handles,
labels=labels,
loc="upper right",
fontsize=8,
framealpha=0.95,
facecolor=ELEVATED_BG,
edgecolor=INK_SOFT,
fancybox=True,
borderpad=0.8,
)
for text in leg.get_texts():
text.set_color(INK)
# Clean axes
ax.set_xlim(-1.3, 1.3)
ax.set_ylim(-1.3, 1.3)
ax.set_xticks([])
ax.set_yticks([])
ax.set_xlabel("")
ax.set_ylabel("")
for spine in ax.spines.values():
spine.set_visible(False)
ax.set_title("maze-circular · python · seaborn · anyplot.ai", fontsize=12, fontweight="medium", color=INK, pad=10)
plt.tight_layout()
plt.savefig(f"plot-{THEME}.png", dpi=400, bbox_inches="tight", facecolor=PAGE_BG)
Part of Circular Maze Puzzle on anyplot.ai.