{"spec_id":"crossword-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// crossword-basic: Crossword Puzzle Grid\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 85/100 | Updated: 2026-09-02\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// The block/entry pattern is data (like the Imprint palette), not theme chrome:\n// it must render identically in both themes, so these fills are fixed hex\n// literals rather than theme-adaptive tokens. Only stroke/frame/title follow\n// the theme.\nconst BLOCK_FILL = \"#1A1A17\";\nconst ENTRY_FILL = \"#FAF8F1\";\nconst NUMBER_FILL = \"#4A4A44\";\n\n// --- Data: symmetric 13x13 block pattern (180-degree rotational symmetry) --\n// Only the top half (rows 0-6, row 6 is the center row) is authored by hand;\n// the bottom half is derived by point-reflection so symmetry is exact by\n// construction rather than by manually mirrored coordinates.\nconst N = 13;\nconst half = [\n  \".....#.#.....\",\n  \".....#.#.....\",\n  \"##.........##\",\n  \"...#.....#...\",\n  \"......#......\",\n  \".#.........#.\",\n  \"....#.#.#....\",\n];\nconst grid = [];\nfor (let r = 0; r <= 6; r++) {\n  grid[r] = half[r].split(\"\").map((ch) => (ch === \"#\" ? 1 : 0));\n}\nfor (let r = 7; r < N; r++) {\n  grid[r] = grid[N - 1 - r].slice().reverse();\n}\n\n// --- Clue numbering: standard newspaper-crossword rule -----------------\n// A white cell starts a number if it opens an across run (no white cell to\n// its left, a white cell to its right) or a down run (no white cell above,\n// a white cell below). Numbers increment in reading order.\nconst numbers = new Map();\nlet clueNumber = 1;\nfor (let r = 0; r < N; r++) {\n  for (let c = 0; c < N; c++) {\n    if (grid[r][c] === 1) continue;\n    const startsAcross =\n      (c === 0 || grid[r][c - 1] === 1) && c + 1 < N && grid[r][c + 1] === 0;\n    const startsDown =\n      (r === 0 || grid[r - 1][c] === 1) && r + 1 < N && grid[r + 1][c] === 0;\n    if (startsAcross || startsDown) {\n      numbers.set(`${r},${c}`, clueNumber++);\n    }\n  }\n}\n\n// --- Layout -----------------------------------------------------------------\nconst margin = { top: 120, right: 80, bottom: 40, left: 80 };\nconst cell = (width - margin.left - margin.right) / N;\nconst gridX = margin.left;\nconst gridY = margin.top;\n\n// --- SVG mount ----------------------------------------------------------\nconst svg = d3\n  .select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${gridX},${gridY})`);\n\n// --- Cells: white entry squares, black blocking squares -----------------\nconst cells = d3.cross(d3.range(N), d3.range(N)).map(([r, c]) => ({\n  r,\n  c,\n  blocked: grid[r][c] === 1,\n  number: numbers.get(`${r},${c}`),\n}));\n\ng.selectAll(\"rect.cell\")\n  .data(cells)\n  .join(\"rect\")\n  .attr(\"class\", \"cell\")\n  .attr(\"x\", (d) => d.c * cell)\n  .attr(\"y\", (d) => d.r * cell)\n  .attr(\"width\", cell)\n  .attr(\"height\", cell)\n  .attr(\"fill\", (d) => (d.blocked ? BLOCK_FILL : ENTRY_FILL))\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5);\n\n// --- Clue numbers, top-left corner of each word-start cell --------------\n// Fixed dark fill: numbers always sit on the fixed off-white ENTRY_FILL, so\n// the color must not flip to a light tone in the dark theme.\ng.selectAll(\"text.clue\")\n  .data(cells.filter((d) => d.number !== undefined))\n  .join(\"text\")\n  .attr(\"class\", \"clue\")\n  .attr(\"x\", (d) => d.c * cell + 5)\n  .attr(\"y\", (d) => d.r * cell + 16)\n  .attr(\"fill\", NUMBER_FILL)\n  .style(\"font-size\", \"15px\")\n  .style(\"font-family\", \"sans-serif\")\n  .text((d) => d.number);\n\n// --- Outer frame --------------------------------------------------------\ng.append(\"rect\")\n  .attr(\"x\", 0)\n  .attr(\"y\", 0)\n  .attr(\"width\", N * cell)\n  .attr(\"height\", N * cell)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 2.5);\n\n// --- Title ----------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 62)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .style(\"font-family\", \"sans-serif\")\n  .text(\"crossword-basic · javascript · d3 · anyplot.ai\");\n"}