{"spec_id":"maze-circular","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nmaze-circular: Circular Maze Puzzle\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-05-20\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove this file's directory from sys.path to avoid circular import with the altair package\nif sys.path and os.path.exists(os.path.join(sys.path[0] or \".\", \"altair.py\")):\n    sys.path = sys.path[1:]\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nWALL_COLOR = \"#1A1A17\" if THEME == \"light\" else \"#E0DFD8\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nENTRY_COLOR = \"#4467A3\"  # Okabe-Ito position 3, theme-independent (same in light and dark)\nGOAL_COLOR = \"#AE3030\"  # Okabe-Ito position 5, theme-independent\n\n# Difficulty: \"easy\" / \"medium\" / \"hard\" — scales sector density (more sectors = harder maze)\nDIFFICULTY = os.getenv(\"ANYPLOT_DIFFICULTY\", \"medium\")\ndensity_map = {\"easy\": 0.70, \"medium\": 1.0, \"hard\": 1.40}\ndensity = density_map.get(DIFFICULTY, 1.0)\n\n# Maze parameters\nnp.random.seed(42)\nnum_rings = 7\nsectors_per_ring = [1] + [max(4, round((12 + i * 4) * density)) for i in range(num_rings)]\nring_width = 1.0\n\n# Data structures for maze walls\nwalls = []\nfor ring in range(num_rings):\n    ring_walls = []\n    for _sector in range(sectors_per_ring[ring + 1]):\n        ring_walls.append({\"outer\": True, \"cw\": True})\n    walls.append(ring_walls)\n\n# Union-Find for Kruskal's maze generation (inlined, no helper functions)\nparent = {}\nfor ring in range(num_rings):\n    for sector in range(sectors_per_ring[ring + 1]):\n        parent[(ring, sector)] = (ring, sector)\n\npossible_walls = []\nfor ring in range(num_rings):\n    num_sectors = sectors_per_ring[ring + 1]\n    for sector in range(num_sectors):\n        next_sector = (sector + 1) % num_sectors\n        possible_walls.append((\"cw\", ring, sector, ring, next_sector))\n        if ring < num_rings - 1:\n            outer_sectors = sectors_per_ring[ring + 2]\n            ratio = outer_sectors / num_sectors\n            outer_sector = int(sector * ratio)\n            possible_walls.append((\"outer\", ring, sector, ring + 1, outer_sector))\n\nnp.random.shuffle(possible_walls)\n\nfor wall_type, r1, s1, r2, s2 in possible_walls:\n    cell1, cell2 = (r1, s1), (r2, s2)\n    root1 = cell1\n    while parent[root1] != root1:\n        root1 = parent[root1]\n    c = cell1\n    while parent[c] != root1:\n        parent[c], c = root1, parent[c]\n\n    root2 = cell2\n    while parent[root2] != root2:\n        root2 = parent[root2]\n    c = cell2\n    while parent[c] != root2:\n        parent[c], c = root2, parent[c]\n\n    if root1 != root2:\n        parent[root1] = root2\n        if wall_type == \"cw\":\n            walls[r1][s1][\"cw\"] = False\n        else:\n            walls[r1][s1][\"outer\"] = False\n\n# Entry fixed at top (θ ≈ π/2) so START label is always clearly readable\nentry_sector = sectors_per_ring[num_rings] // 4\n\n# Generate wall segment coordinates\nouter_boundary_data = []  # Outer boundary — rendered thicker for visual emphasis\nwall_data = []  # Interior walls\nwall_count = 0\n\ntheta_vals = np.linspace(0, 2 * np.pi, sectors_per_ring[num_rings] + 1)\nentry_theta_start = theta_vals[entry_sector]\nentry_theta_end = theta_vals[entry_sector + 1]\nouter_r = num_rings * ring_width\n\n# Outer boundary arcs with gap at entry — tracked separately for thicker strokeWidth\nif entry_sector > 0:\n    theta = np.linspace(0, entry_theta_start, 60)\n    x_arr, y_arr = outer_r * np.cos(theta), outer_r * np.sin(theta)\n    for i in range(len(x_arr)):\n        outer_boundary_data.append({\"x\": x_arr[i], \"y\": y_arr[i], \"wall_id\": f\"ob_{wall_count}\", \"order\": i})\n    wall_count += 1\n\nif entry_sector < sectors_per_ring[num_rings] - 1:\n    theta = np.linspace(entry_theta_end, 2 * np.pi, 60)\n    x_arr, y_arr = outer_r * np.cos(theta), outer_r * np.sin(theta)\n    for i in range(len(x_arr)):\n        outer_boundary_data.append({\"x\": x_arr[i], \"y\": y_arr[i], \"wall_id\": f\"ob_{wall_count}\", \"order\": i})\n    wall_count += 1\n\n# Concentric ring walls (arcs with gaps at passages)\nfor ring in range(num_rings - 1):\n    r = (ring + 1) * ring_width\n    num_sectors = sectors_per_ring[ring + 1]\n    ring_theta = np.linspace(0, 2 * np.pi, num_sectors + 1)\n    arc_start = None\n\n    for sector in range(num_sectors):\n        if walls[ring][sector][\"outer\"]:\n            if arc_start is None:\n                arc_start = ring_theta[sector]\n        else:\n            if arc_start is not None:\n                theta = np.linspace(arc_start, ring_theta[sector], 30)\n                x_arr, y_arr = r * np.cos(theta), r * np.sin(theta)\n                for i in range(len(x_arr)):\n                    wall_data.append({\"x\": x_arr[i], \"y\": y_arr[i], \"wall_id\": f\"r_{ring}_{wall_count}\", \"order\": i})\n                wall_count += 1\n                arc_start = None\n\n    if arc_start is not None:\n        theta = np.linspace(arc_start, 2 * np.pi, 30)\n        x_arr, y_arr = r * np.cos(theta), r * np.sin(theta)\n        for i in range(len(x_arr)):\n            wall_data.append({\"x\": x_arr[i], \"y\": y_arr[i], \"wall_id\": f\"r_{ring}_{wall_count}\", \"order\": i})\n        wall_count += 1\n\n# Radial walls (sector dividers with gaps at passages)\nfor ring in range(num_rings):\n    num_sectors = sectors_per_ring[ring + 1]\n    r_inner = ring * ring_width if ring > 0 else 0.3\n    r_outer = (ring + 1) * ring_width\n    radial_theta = np.linspace(0, 2 * np.pi, num_sectors + 1)\n\n    for sector in range(num_sectors):\n        if walls[ring][sector][\"cw\"]:\n            theta_val = radial_theta[sector + 1] if sector < num_sectors - 1 else 2 * np.pi\n            x1, y1 = r_inner * np.cos(theta_val), r_inner * np.sin(theta_val)\n            x2, y2 = r_outer * np.cos(theta_val), r_outer * np.sin(theta_val)\n            wid = f\"rad_{ring}_{sector}_{wall_count}\"\n            wall_data.append({\"x\": x1, \"y\": y1, \"wall_id\": wid, \"order\": 0})\n            wall_data.append({\"x\": x2, \"y\": y2, \"wall_id\": wid, \"order\": 1})\n            wall_count += 1\n\n# DataFrames\ndf_outer = pd.DataFrame(outer_boundary_data)\ndf_walls = pd.DataFrame(wall_data)\n\n# Goal at center: filled halo + star symbol\ngoal_halo_df = pd.DataFrame({\"x\": [0.0], \"y\": [0.0]})\ngoal_star_df = pd.DataFrame({\"x\": [0.0], \"y\": [0.0], \"label\": [\"★\"]})\n\n# Entry label and inward-pointing arrow indicator\nentry_angle = (entry_theta_start + entry_theta_end) / 2\nentry_label_r = outer_r + 0.90\nentry_arrow_r = outer_r + 0.30\n\nentry_df = pd.DataFrame(\n    {\"x\": [entry_label_r * np.cos(entry_angle)], \"y\": [entry_label_r * np.sin(entry_angle)], \"label\": [\"START\"]}\n)\narrow_df = pd.DataFrame(\n    {\"x\": [entry_arrow_r * np.cos(entry_angle)], \"y\": [entry_arrow_r * np.sin(entry_angle)], \"label\": [\"▼\"]}\n)\n\nmax_extent = outer_r + 1.6\n\n# Shared scale domains to guarantee equal-aspect circle\nx_scale = alt.Scale(domain=[-max_extent, max_extent])\ny_scale = alt.Scale(domain=[-max_extent, max_extent])\n\n# Outer boundary wall lines — thicker for visual boundary emphasis\nouter_chart = (\n    alt.Chart(df_outer)\n    .mark_line(color=WALL_COLOR, strokeWidth=4.0)\n    .encode(\n        x=alt.X(\"x:Q\", axis=None, scale=x_scale),\n        y=alt.Y(\"y:Q\", axis=None, scale=y_scale),\n        detail=\"wall_id:N\",\n        order=\"order:O\",\n    )\n)\n\n# Interior wall lines\nwalls_chart = (\n    alt.Chart(df_walls)\n    .mark_line(color=WALL_COLOR, strokeWidth=2.5)\n    .encode(\n        x=alt.X(\"x:Q\", axis=None, scale=x_scale),\n        y=alt.Y(\"y:Q\", axis=None, scale=y_scale),\n        detail=\"wall_id:N\",\n        order=\"order:O\",\n    )\n)\n\n# Goal halo — subtle filled circle marking the center zone\ngoal_halo_chart = (\n    alt.Chart(goal_halo_df)\n    .mark_point(color=GOAL_COLOR, size=350, filled=True, opacity=0.30)\n    .encode(x=alt.X(\"x:Q\", axis=None, scale=x_scale), y=alt.Y(\"y:Q\", axis=None, scale=y_scale))\n)\n\n# Goal star at center — Unicode ★ for reliable rendering\ngoal_chart = (\n    alt.Chart(goal_star_df)\n    .mark_text(fontSize=24, fontWeight=\"bold\", color=GOAL_COLOR)\n    .encode(x=alt.X(\"x:Q\", axis=None, scale=x_scale), y=alt.Y(\"y:Q\", axis=None, scale=y_scale), text=\"label:N\")\n)\n\n# Inward-pointing arrow at entry gap (▼ points toward center when entry is at top)\narrow_chart = (\n    alt.Chart(arrow_df)\n    .mark_text(fontSize=14, color=ENTRY_COLOR)\n    .encode(x=alt.X(\"x:Q\", axis=None, scale=x_scale), y=alt.Y(\"y:Q\", axis=None, scale=y_scale), text=\"label:N\")\n)\n\n# Entry label above the maze\nentry_chart = (\n    alt.Chart(entry_df)\n    .mark_text(fontSize=18, fontWeight=\"bold\", color=ENTRY_COLOR)\n    .encode(x=alt.X(\"x:Q\", axis=None, scale=x_scale), y=alt.Y(\"y:Q\", axis=None, scale=y_scale), text=\"label:N\")\n)\n\nchart = (\n    alt.layer(outer_chart, walls_chart, goal_halo_chart, goal_chart, arrow_chart, entry_chart)\n    .properties(width=600, height=600, background=PAGE_BG, title=\"maze-circular · python · altair · anyplot.ai\")\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_axis(grid=False)\n    .configure_title(fontSize=16, color=INK)\n)\n\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\nchart.save(f\"plot-{THEME}.html\")\n"}