{"spec_id":"crossword-basic","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\ncrossword-basic: Crossword Puzzle Grid\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-05-20\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    coord_fixed,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_text,\n    geom_tile,\n    ggplot,\n    labs,\n    scale_fill_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n)\n\n\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# Create 15x15 crossword grid with 180-degree rotational symmetry\nsize = 15\n\ngrid = np.zeros((size, size), dtype=int)\n\nblack_positions = [\n    (0, 4),\n    (0, 10),\n    (1, 4),\n    (1, 10),\n    (2, 4),\n    (2, 7),\n    (2, 10),\n    (3, 0),\n    (3, 7),\n    (3, 11),\n    (3, 12),\n    (3, 13),\n    (3, 14),\n    (4, 0),\n    (4, 1),\n    (4, 5),\n    (4, 6),\n    (4, 9),\n    (5, 5),\n    (5, 11),\n    (6, 2),\n    (6, 3),\n    (6, 8),\n    (6, 13),\n    (6, 14),\n    (7, 7),\n]\n\nfor r, c in black_positions:\n    grid[r, c] = 1\n    grid[size - 1 - r, size - 1 - c] = 1\n\n# Build tile DataFrame\ndata = []\nfor r in range(size):\n    for c in range(size):\n        data.append({\"col\": c + 1, \"row\": size - r, \"cell_type\": \"blocked\" if grid[r, c] == 1 else \"entry\"})\n\ndf = pd.DataFrame(data)\n\n# Determine word start positions (cells beginning an across or down word)\nword_starts = {}\nnum = 1\nfor r in range(size):\n    for c in range(size):\n        if grid[r, c] == 1:\n            continue\n        starts_across = (c == 0 or grid[r, c - 1] == 1) and c < size - 1 and grid[r, c + 1] == 0\n        starts_down = (r == 0 or grid[r - 1, c] == 1) and r < size - 1 and grid[r + 1, c] == 0\n        if starts_across or starts_down:\n            word_starts[(r, c)] = num\n            num += 1\n\nnumber_data = []\nfor (r, c), clue_num in word_starts.items():\n    number_data.append({\"col\": c + 1, \"row\": size - r, \"label\": str(clue_num)})\n\nnumbers_df = pd.DataFrame(number_data)\n\n# Cell fills: crossword convention — entry cells use a warm tint in light mode for visual depth\nENTRY_FILL = \"#F5F3EC\" if THEME == \"light\" else \"#FFFFFF\"\nBLOCKED_FILL = \"#1A1A1A\"\n\nplot = (\n    ggplot(df, aes(x=\"col\", y=\"row\", fill=\"cell_type\"))\n    + geom_tile(color=INK_SOFT, size=0.4)\n    + scale_fill_manual(values={\"entry\": ENTRY_FILL, \"blocked\": BLOCKED_FILL})\n    + geom_text(\n        data=numbers_df,\n        mapping=aes(x=\"col\", y=\"row\", label=\"label\"),\n        size=6,\n        ha=\"left\",\n        va=\"top\",\n        nudge_x=-0.35,\n        nudge_y=0.35,\n        color=\"#333333\",\n        inherit_aes=False,\n    )\n    + scale_x_continuous(breaks=[], expand=(0, 0))\n    + scale_y_continuous(breaks=[], expand=(0, 0))\n    + coord_fixed(ratio=1)\n    + labs(\n        title=\"crossword-basic · python · plotnine · anyplot.ai\",\n        subtitle=\"180° rotational symmetry · 79 numbered positions\",\n    )\n    + theme(\n        figure_size=(6, 6),\n        plot_title=element_text(size=13, ha=\"center\", color=INK, weight=\"bold\"),\n        plot_subtitle=element_text(size=8, ha=\"center\", color=INK_SOFT),\n        axis_text=element_blank(),\n        axis_title=element_blank(),\n        axis_ticks=element_blank(),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_border=element_rect(color=INK_SOFT, fill=None, size=0.8),\n        panel_grid_major=element_blank(),\n        panel_grid_minor=element_blank(),\n        legend_position=\"none\",\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        plot_margin=0.02,\n    )\n)\n\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=6, height=6, units=\"in\")\n"}