{"spec_id":"crossword-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\ncrossword-basic: Crossword Puzzle Grid\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 84/100 | Updated: 2026-05-20\n\"\"\"\n\nimport sys\nfrom pathlib import Path\n\n\n_script_dir = str(Path(__file__).parent.absolute())\nsys.path = [p for p in sys.path if p != _script_dir and p != \"\"]\n\nimport os\nimport time\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, LabelSet\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\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# Warmer off-white for entry cells — avoids harsh pure-white against the warm page background\nCELL_BG = \"#FFFDF6\" if THEME == \"light\" else \"#FFFFFF\"\n\n# Data — 15x15 crossword grid with 180-degree rotational symmetry\nnp.random.seed(42)\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, 6),\n    (3, 11),\n    (3, 12),\n    (4, 5),\n    (4, 9),\n    (5, 3),\n    (5, 8),\n    (5, 13),\n    (6, 2),\n    (6, 7),\n    (6, 12),\n    (7, 0),\n    (7, 6),\n    (7, 8),\n    (7, 14),\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# Clue numbering: cells that start an across or down word\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# Separate cell data for rendering, hover, and clue number labels\nwhite_x, white_y = [], []\nblack_x, black_y = [], []\nnumbered_x, numbered_y, numbered_label = [], [], []\nlabel_x, label_y, label_text = [], [], []\n\nfor r in range(grid_size):\n    for c in range(grid_size):\n        y = grid_size - 1 - r  # flip so row 0 is at top\n        if grid[r, c] == 0:\n            white_x.append(c)\n            white_y.append(y)\n            if (r, c) in numbers:\n                numbered_x.append(c)\n                numbered_y.append(y)\n                numbered_label.append(f\"Clue {numbers[(r, c)]}\")\n                label_x.append(c - 0.43)\n                label_y.append(y + 0.18)\n                label_text.append(str(numbers[(r, c)]))\n        else:\n            black_x.append(c)\n            black_y.append(y)\n\n# Plot — square canvas for 1:1 cell aspect ratio\np = figure(\n    width=2400,\n    height=2400,\n    title=\"crossword-basic · python · bokeh · anyplot.ai\",\n    x_range=(-0.6, grid_size - 0.4),\n    y_range=(-0.6, grid_size - 0.4),\n    toolbar_location=None,\n    min_border_bottom=80,\n    min_border_left=80,\n    min_border_top=110,\n    min_border_right=50,\n)\n\np.xaxis.visible = False\np.yaxis.visible = False\np.xgrid.visible = False\np.ygrid.visible = False\n\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\np.title.text_font_size = \"50pt\"\np.title.align = \"center\"\np.title.text_color = INK\n\n# White entry cells — warm off-white fill to complement the page surface\nwhite_source = ColumnDataSource(data={\"x\": white_x, \"y\": white_y})\np.rect(\n    x=\"x\", y=\"y\", source=white_source, width=0.96, height=0.96, fill_color=CELL_BG, line_color=INK_SOFT, line_width=1.5\n)\n\n# Transparent overlay on numbered cells — hover targets without visual double-drawing\nnumbered_source = ColumnDataSource(data={\"x\": numbered_x, \"y\": numbered_y, \"label\": numbered_label})\nnumbered_renderer = p.rect(x=\"x\", y=\"y\", source=numbered_source, width=0.96, height=0.96, fill_alpha=0, line_alpha=0)\nhover = HoverTool(renderers=[numbered_renderer], tooltips=[(\"\", \"@label\")])\np.add_tools(hover)\n\n# Black blocking cells\nblack_source = ColumnDataSource(data={\"x\": black_x, \"y\": black_y})\np.rect(\n    x=\"x\",\n    y=\"y\",\n    source=black_source,\n    width=0.96,\n    height=0.96,\n    fill_color=\"#111110\",\n    line_color=\"#111110\",\n    line_width=1.5,\n)\n\n# Outer perimeter border — thicker frame gives the puzzle a polished, bounded appearance\np.rect(x=7, y=7, width=15.0, height=15.0, fill_alpha=0, line_color=INK, line_width=5)\n\n# Clue numbers via LabelSet — idiomatic Bokeh, batched from ColumnDataSource\nlabel_source = ColumnDataSource(data={\"lx\": label_x, \"ly\": label_y, \"lt\": label_text})\nlabels = LabelSet(\n    x=\"lx\", y=\"ly\", text=\"lt\", source=label_source, text_font_size=\"20pt\", text_font_style=\"bold\", text_color=INK_SOFT\n)\np.add_layout(labels)\n\n# Save HTML (interactive artifact)\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot via headless Chrome (export_png not used — snap driver conflict)\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()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}