{"spec_id":"crossword-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\ncrossword-basic: Crossword Puzzle Grid\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-20\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent the script's own directory from shadowing the 'altair' package\nsys.path = [p for p in sys.path if not p.endswith(\"/python\")]\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\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Crossword cell colors (theme-adaptive monochrome design)\nCELL_ENTRY = \"#FFFFFF\" if THEME == \"light\" else \"#DEDAD3\"\nCELL_BLOCK = \"#1A1A17\" if THEME == \"light\" else \"#3E3E3A\"\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, 3),\n    (4, 8),\n    (4, 13),\n    (4, 14),\n    (5, 5),\n    (5, 9),\n    (6, 2),\n    (6, 7),\n    (6, 12),\n    (7, 2),\n    (7, 7),\n    (7, 12),\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# Determine numbered cells (start of across or down words)\nnumbers = {}\nclue_num = 1\n\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# Build dataframes\ncells_data = [\n    {\"row\": r, \"col\": c, \"color\": CELL_BLOCK if grid[r, c] == 1 else CELL_ENTRY}\n    for r in range(grid_size)\n    for c in range(grid_size)\n]\ncells_df = pd.DataFrame(cells_data)\n\nnumbers_df = pd.DataFrame([{\"row\": r, \"col\": c, \"number\": str(num)} for (r, c), num in numbers.items()])\n\n# Grid cells layer\ncells = (\n    alt.Chart(cells_df)\n    .mark_rect(stroke=INK_SOFT, strokeWidth=1.5)\n    .encode(x=alt.X(\"col:O\", axis=None), y=alt.Y(\"row:O\", axis=None), color=alt.Color(\"color:N\", scale=None))\n)\n\n# Clue numbers overlay\nclue_numbers = (\n    alt.Chart(numbers_df)\n    .mark_text(align=\"left\", baseline=\"top\", dx=-12, dy=-12, fontSize=10, fontWeight=\"bold\", color=INK_SOFT)\n    .encode(x=alt.X(\"col:O\", axis=None), y=alt.Y(\"row:O\", axis=None), text=\"number:N\")\n)\n\n# Compose chart — square 600×600 canvas → 2400×2400 px at scale=4\nchart = (\n    (cells + clue_numbers)\n    .properties(\n        width=600,\n        height=600,\n        background=PAGE_BG,\n        title=alt.Title(\n            \"crossword-basic · python · altair · anyplot.ai\", fontSize=16, anchor=\"middle\", offset=18, color=INK\n        ),\n    )\n    .configure_view(fill=PAGE_BG, strokeWidth=0)\n)\n\n# Save outputs\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\nchart.save(f\"plot-{THEME}.html\")\n"}