{"spec_id":"maze-circular","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nmaze-circular: Circular Maze Puzzle\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-05-20\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, Label\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Maze parameters — difficulty controls ring count\nnp.random.seed(42)\ndifficulty = \"medium\"  # easy=5 rings, medium=7 rings, hard=9 rings\nrings_map = {\"easy\": 5, \"medium\": 7, \"hard\": 9}\nrings = rings_map[difficulty]\nbase_sectors = [8, 12, 16, 20, 24, 28, 32, 36, 40]\nsectors_per_ring = base_sectors[:rings]\n\nwall_color = INK\nwall_width = 5\nentry_color = \"#009E73\"  # Okabe-Ito position 1 (brand green)\ngoal_color = \"#AE3030\"  # Okabe-Ito position 5 (orange)\n\n# Build maze cells: (ring, sector) -> {visited, walls}\ncells = {}\nfor r in range(rings):\n    n_sec = sectors_per_ring[r]\n    for s in range(n_sec):\n        cells[(r, s)] = {\"visited\": False, \"walls\": {\"inner\": True, \"outer\": True, \"cw\": True, \"ccw\": True}}\n\n# Iterative recursive backtracking — neighbors computed inline, no helper functions\nstack = [(0, 0)]\ncells[(0, 0)][\"visited\"] = True\n\nwhile stack:\n    ring, sector = stack[-1]\n    n_sec = sectors_per_ring[ring]\n\n    # Same-ring neighbors (clockwise and counter-clockwise)\n    neighbors = [((ring, (sector - 1) % n_sec), \"ccw\", \"cw\"), ((ring, (sector + 1) % n_sec), \"cw\", \"ccw\")]\n    # Inner ring neighbor\n    if ring > 0:\n        inner_n = sectors_per_ring[ring - 1]\n        neighbors.append(((ring - 1, int(sector * inner_n / n_sec)), \"inner\", \"outer\"))\n    # Outer ring neighbor\n    if ring < rings - 1:\n        outer_n = sectors_per_ring[ring + 1]\n        neighbors.append(((ring + 1, int(sector * outer_n / n_sec)), \"outer\", \"inner\"))\n\n    unvisited = [(n, w, ow) for n, w, ow in neighbors if n in cells and not cells[n][\"visited\"]]\n\n    if unvisited:\n        next_cell, wall_to_remove, opp_wall = unvisited[np.random.randint(len(unvisited))]\n        cells[(ring, sector)][\"walls\"][wall_to_remove] = False\n        cells[next_cell][\"walls\"][opp_wall] = False\n        cells[next_cell][\"visited\"] = True\n        stack.append(next_cell)\n    else:\n        stack.pop()\n\n# Open entry gap on outermost ring\nentry_sector = 0\ncells[(rings - 1, entry_sector)][\"walls\"][\"outer\"] = False\n\n# Figure — 2400×2400 square canvas for circular maze\np = figure(\n    width=2400,\n    height=2400,\n    title=\"maze-circular · python · bokeh · anyplot.ai\",\n    x_range=(-1.20, 1.20),\n    y_range=(-1.20, 1.20),\n    background_fill_color=PAGE_BG,\n    border_fill_color=PAGE_BG,\n    toolbar_location=None,\n    match_aspect=True,\n    min_border_bottom=60,\n    min_border_left=60,\n    min_border_top=130,\n    min_border_right=60,\n)\n\n# Hide axes, grid, and figure outline\np.axis.visible = False\np.grid.visible = False\np.outline_line_color = None\n\n# Title\np.title.text_font_size = \"50pt\"\np.title.text_font_style = \"bold\"\np.title.align = \"center\"\np.title.text_color = INK\n\n# Ring radii from center hub to outer boundary\nring_radii = np.linspace(0.12, 0.95, rings + 1)\n\n# Draw maze walls: outer arc and radial (cw side only avoids double-drawing)\nfor r in range(rings):\n    n_sec = sectors_per_ring[r]\n    sector_angle = 2 * np.pi / n_sec\n    inner_radius = ring_radii[r]\n    outer_radius = ring_radii[r + 1]\n\n    for s in range(n_sec):\n        start_angle = s * sector_angle\n        end_angle = (s + 1) * sector_angle\n\n        if cells[(r, s)][\"walls\"][\"outer\"]:\n            arc_pts = np.linspace(start_angle, end_angle, 50)\n            p.line(\n                outer_radius * np.cos(arc_pts),\n                outer_radius * np.sin(arc_pts),\n                line_width=wall_width,\n                line_color=wall_color,\n            )\n\n        if cells[(r, s)][\"walls\"][\"cw\"]:\n            p.line(\n                [inner_radius * np.cos(end_angle), outer_radius * np.cos(end_angle)],\n                [inner_radius * np.sin(end_angle), outer_radius * np.sin(end_angle)],\n                line_width=wall_width,\n                line_color=wall_color,\n            )\n\n# Outer boundary circle with entry gap\nouter_r = ring_radii[-1]\nouter_n = sectors_per_ring[-1]\nentry_start_angle = entry_sector * (2 * np.pi / outer_n)\nentry_end_angle = (entry_sector + 1) * (2 * np.pi / outer_n)\n\nboundary_pts = np.linspace(entry_end_angle, entry_start_angle + 2 * np.pi, 360)\np.line(outer_r * np.cos(boundary_pts), outer_r * np.sin(boundary_pts), line_width=wall_width + 4, line_color=wall_color)\n\n# Inner boundary (central hub)\ntheta = np.linspace(0, 2 * np.pi, 120)\np.line(ring_radii[0] * np.cos(theta), ring_radii[0] * np.sin(theta), line_width=wall_width, line_color=wall_color)\n\n# Center goal circle — ColumnDataSource enables HoverTool tooltip\ngoal_r = ring_radii[0] * 0.65\ngoal_source = ColumnDataSource(\n    data={\n        \"xs\": [list(goal_r * np.cos(theta))],\n        \"ys\": [list(goal_r * np.sin(theta))],\n        \"label\": [\"GOAL — navigate here to win!\"],\n    }\n)\ngoal_renderer = p.patches(\n    xs=\"xs\", ys=\"ys\", fill_color=goal_color, line_color=wall_color, line_width=3, source=goal_source\n)\ngoal_hover = HoverTool(renderers=[goal_renderer], tooltips=[(\"\", \"@label\")])\np.add_tools(goal_hover)\n\np.add_layout(\n    Label(x=0, y=-0.01, text=\"★\", text_font_size=\"28pt\", text_align=\"center\", text_baseline=\"middle\", text_color=INK)\n)\n\n# Entry triangle marker — ColumnDataSource enables HoverTool tooltip\nentry_angle = (entry_start_angle + entry_end_angle) / 2\nentry_x = 1.04 * np.cos(entry_angle)\nentry_y = 1.04 * np.sin(entry_angle)\nentry_source = ColumnDataSource(\n    data={\n        \"x\": [entry_x],\n        \"y\": [entry_y],\n        \"angle\": [entry_angle - np.pi / 2],\n        \"label\": [\"START — begin your journey here!\"],\n    }\n)\nentry_renderer = p.scatter(\n    x=\"x\",\n    y=\"y\",\n    marker=\"triangle\",\n    size=30,\n    fill_color=entry_color,\n    line_color=wall_color,\n    angle=\"angle\",\n    source=entry_source,\n)\nentry_hover = HoverTool(renderers=[entry_renderer], tooltips=[(\"\", \"@label\")])\np.add_tools(entry_hover)\n\np.add_layout(\n    Label(\n        x=entry_x * 1.09,\n        y=entry_y * 1.09,\n        text=\"START\",\n        text_font_size=\"28pt\",\n        text_align=\"center\",\n        text_baseline=\"middle\",\n        text_color=entry_color,\n        text_font_style=\"bold\",\n    )\n)\n\n# Difficulty annotation below the maze\np.add_layout(\n    Label(\n        x=0,\n        y=-1.12,\n        text=f\"{rings} rings · {difficulty}\",\n        text_font_size=\"24pt\",\n        text_align=\"center\",\n        text_baseline=\"middle\",\n        text_color=INK_SOFT,\n        text_font_style=\"italic\",\n    )\n)\n\n# Save HTML catalog artifact\noutput_file(f\"plot-{THEME}.html\", title=\"Circular Maze Puzzle\")\nsave(p)\n\n# Screenshot via headless Chrome (Selenium — export_png not available in CI)\nW, H = 2400, 2400\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\n# Force page background to match PAGE_BG — prevents thin lighter border in dark theme screenshots\ndriver.execute_script(\n    f\"document.documentElement.style.background='{PAGE_BG}';document.body.style.background='{PAGE_BG}';\"\n)\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}