{"spec_id":"crossword-basic","library":"makie","language":"julia","code":"# anyplot.ai\n# crossword-basic: Crossword Puzzle Grid\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 85/100 | Updated: 2026-09-02\n\nusing CairoMakie\nusing Colors\n\n# --- Theme tokens -------------------------------------------------------------\nconst THEME    = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG  = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst INK      = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\n\n# Fixed, theme-invariant data-encoding colors for the grid itself. The\n# black/white cell pattern IS the plot's data (newspaper-crossword\n# convention), so it must render identically in both themes — unlike the\n# chrome tokens above, which are meant to flip.\nconst CELL_BLOCKED = colorant\"#1A1A17\"\nconst CELL_ENTRY   = colorant\"#FFFDF6\"\n\n# --- Data ----------------------------------------------------------------------\n# 15x15 grid; blocking cells carry traditional 180-degree rotational symmetry.\n# Seeding only the upper half and mirroring each seed guarantees the symmetry\n# by construction rather than by manually checking a hand-typed matrix.\nconst N = 15\n\nseed_blocks = [\n    (1, 4), (1, 11), (2, 4), (2, 11), (3, 4), (3, 8), (3, 11),\n    (4, 1), (4, 2), (4, 7), (5, 5), (5, 10), (6, 3), (6, 6),\n    (7, 7), (8, 1), (8, 4),\n]\n\nblocked = falses(N, N)\nfor (r, c) in seed_blocks\n    blocked[r, c] = true\n    blocked[N + 1 - r, N + 1 - c] = true\nend\n\n# Word-start numbering: a cell is numbered when it opens an across and/or a\n# down entry, scanning left-to-right then top-to-bottom (standard convention).\nnumbers = zeros(Int, N, N)\ncounter = Ref(1)\nn_across = Ref(0)\nn_down = Ref(0)\nfor r in 1:N, c in 1:N\n    blocked[r, c] && continue\n    starts_across = (c == 1 || blocked[r, c - 1]) && (c < N && !blocked[r, c + 1])\n    starts_down = (r == 1 || blocked[r - 1, c]) && (r < N && !blocked[r + 1, c])\n    starts_across && (n_across[] += 1)\n    starts_down && (n_down[] += 1)\n    if starts_across || starts_down\n        numbers[r, c] = counter[]\n        counter[] += 1\n    end\nend\n\n# --- Plot ------------------------------------------------------------------\nfig = Figure(\n    resolution = (1200, 1200),\n    fontsize = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title = \"crossword-basic · julia · makie · anyplot.ai\",\n    titlesize = 32,\n    titlecolor = INK,\n    aspect = DataAspect(),\n    backgroundcolor = PAGE_BG,\n)\nhidedecorations!(ax)\nhidespines!(ax)\nxlims!(ax, -0.4, N + 0.4)\nylims!(ax, -0.4, N + 0.4)\n\n# Entry cells (white, for letters) vs. blocking cells (black) — this is the\n# plot's data, so both colors are fixed constants that never flip with\n# ANYPLOT_THEME (only the surrounding chrome does).\ncell_rects = [Rect2f(Float32(c - 1), Float32(N - r), 1.0f0, 1.0f0) for r in 1:N for c in 1:N]\ncell_colors = [blocked[r, c] ? CELL_BLOCKED : CELL_ENTRY for r in 1:N for c in 1:N]\npoly!(ax, cell_rects; color = cell_colors, strokewidth = 0)\n\n# Grid lines separating every cell\nfor i in 0:N\n    linesegments!(ax, [Point2f(0, i), Point2f(N, i)]; color = INK_SOFT, linewidth = 1.5)\n    linesegments!(ax, [Point2f(i, 0), Point2f(i, N)]; color = INK_SOFT, linewidth = 1.5)\nend\n\n# Outer frame: a heavier ink rule plus a thin inset accent ruled entirely in\n# the surrounding margin (never crossing live cells) gives the grid the\n# double-ruled \"matted\" border common to printed puzzle books, instead of a\n# single flat outline.\nlines!(\n    ax,\n    [Point2f(0, 0), Point2f(N, 0), Point2f(N, N), Point2f(0, N), Point2f(0, 0)];\n    color = INK,\n    linewidth = 4,\n)\naccent = 0.18\nlines!(\n    ax,\n    [\n        Point2f(-accent, -accent), Point2f(N + accent, -accent),\n        Point2f(N + accent, N + accent), Point2f(-accent, N + accent),\n        Point2f(-accent, -accent),\n    ];\n    color = INK_SOFT,\n    linewidth = 1.5,\n)\n\n# Clue-start numbers, top-left corner of each entry cell — sized up and\n# bolded so they stay legible on a 2400x2400 canvas at mobile widths.\nfor r in 1:N, c in 1:N\n    numbers[r, c] == 0 && continue\n    x = c - 1\n    y = N - r\n    text!(\n        ax,\n        x + 0.09,\n        y + 1 - 0.08;\n        text = string(numbers[r, c]),\n        fontsize = 19,\n        font = :bold,\n        color = INK_SOFT,\n        align = (:left, :top),\n    )\nend\n\n# Subtitle: across/down entry counts add a touch of data storytelling below\n# the grid via a second Figure row (Label), beyond the plain grid alone. The\n# row needs an explicit fixed height — otherwise the DataAspect()-constrained\n# Axis above claims the whole figure and squeezes this row to invisibility.\nLabel(\n    fig[2, 1],\n    \"$(N) × $(N) grid · $(n_across[]) across · $(n_down[]) down\";\n    fontsize = 15,\n    color = INK_SOFT,\n    tellwidth = false,\n    padding = (0, 0, 0, 18),\n)\nrowsize!(fig.layout, 2, Fixed(60))\n\n# --- Save -----------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}