{"spec_id":"crossword-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\ncrossword-basic: Crossword Puzzle Grid\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 85/100 | Updated: 2026-05-20\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\n\n\nLetsPlot.setup_html()\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\"\n\n# Data — 15x15 crossword grid with 180-degree rotational symmetry\ngrid_size = 15\ngrid = np.zeros((grid_size, grid_size), dtype=int)\n\nblack_positions = [\n    (0, 4),\n    (0, 10),\n    (1, 4),\n    (1, 10),\n    (2, 7),\n    (3, 0),\n    (3, 1),\n    (3, 6),\n    (3, 11),\n    (3, 12),\n    (4, 5),\n    (4, 9),\n    (5, 3),\n    (5, 8),\n    (5, 13),\n    (5, 14),\n    (6, 2),\n    (6, 7),\n    (6, 12),\n    (7, 6),\n    (7, 8),\n]\n\nfor r, c in black_positions:\n    grid[r, c] = 1\n    grid[grid_size - 1 - r, grid_size - 1 - c] = 1\n\n# Generate clue numbers\nnumbers = {}\nclue_num = 1\nfor r in range(grid_size):\n    for c in range(grid_size):\n        if grid[r, c] == 1:\n            continue\n        starts_across = (c == 0 or grid[r, c - 1] == 1) and (c < grid_size - 1 and grid[r, c + 1] == 0)\n        starts_down = (r == 0 or grid[r - 1, c] == 1) and (r < grid_size - 1 and grid[r + 1, c] == 0)\n        if starts_across or starts_down:\n            numbers[(r, c)] = clue_num\n            clue_num += 1\n\n# Prepare cell data\ncells_data = []\nfor r in range(grid_size):\n    for c in range(grid_size):\n        cell_type = \"Blocked\" if grid[r, c] == 1 else \"Entry\"\n        cells_data.append({\"x\": c, \"y\": grid_size - 1 - r, \"blocked\": grid[r, c], \"cell_type\": cell_type})\n\ndf_cells = pd.DataFrame(cells_data)\n\n# Prepare number labels with tooltip info\nnumber_labels = []\nfor (r, c), num in numbers.items():\n    direction_parts = []\n    starts_across = (c == 0 or grid[r, c - 1] == 1) and (c < grid_size - 1 and grid[r, c + 1] == 0)\n    starts_down = (r == 0 or grid[r - 1, c] == 1) and (r < grid_size - 1 and grid[r + 1, c] == 0)\n    if starts_across:\n        direction_parts.append(\"Across\")\n    if starts_down:\n        direction_parts.append(\"Down\")\n    number_labels.append(\n        {\n            \"x\": c + 0.05,\n            \"y\": grid_size - 1 - r + 0.35,\n            \"label\": str(num),\n            \"clue\": f\"Clue {num}: {' / '.join(direction_parts)}\",\n        }\n    )\n\ndf_numbers = pd.DataFrame(number_labels)\n\n# Plot\nplot = (\n    ggplot()\n    + geom_tile(\n        data=df_cells[df_cells[\"blocked\"] == 0],\n        mapping=aes(x=\"x\", y=\"y\"),\n        fill=\"white\",\n        color=\"black\",\n        size=0.8,\n        width=0.98,\n        height=0.98,\n        tooltips=layer_tooltips().line(\"@cell_type cell\"),\n    )\n    + geom_tile(\n        data=df_cells[df_cells[\"blocked\"] == 1],\n        mapping=aes(x=\"x\", y=\"y\"),\n        fill=\"black\",\n        color=\"black\",\n        size=0.8,\n        width=0.98,\n        height=0.98,\n        tooltips=layer_tooltips().line(\"@cell_type cell\"),\n    )\n    + geom_text(\n        data=df_numbers,\n        mapping=aes(x=\"x\", y=\"y\", label=\"label\"),\n        size=8,\n        color=\"#1A1A17\",\n        hjust=0,\n        vjust=1,\n        fontface=\"bold\",\n        tooltips=layer_tooltips().line(\"@clue\"),\n    )\n    + coord_fixed(ratio=1)\n    + labs(title=\"crossword-basic · python · letsplot · anyplot.ai\")\n    + theme_void()\n    + theme(\n        plot_title=element_text(size=16, hjust=0.5, face=\"bold\", color=INK),\n        plot_background=element_rect(fill=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n    )\n    + ggsize(600, 600)\n)\n\n# Save\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}