<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Casey Jefferson</title>
    <description>The latest articles on DEV Community by Casey Jefferson (@biblegamesnotes).</description>
    <link>https://dev.to/biblegamesnotes</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4115897%2Fed5da8bb-bd97-4e76-b73f-d830e0414eb5.png</url>
      <title>DEV Community: Casey Jefferson</title>
      <link>https://dev.to/biblegamesnotes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/biblegamesnotes"/>
    <language>en</language>
    <item>
      <title>What I learned writing a crossword generator that runs in the browser</title>
      <dc:creator>Casey Jefferson</dc:creator>
      <pubDate>Tue, 08 Sep 2026 14:06:04 +0000</pubDate>
      <link>https://dev.to/biblegamesnotes/what-i-learned-writing-a-crossword-generator-that-runs-in-the-browser-4fi</link>
      <guid>https://dev.to/biblegamesnotes/what-i-learned-writing-a-crossword-generator-that-runs-in-the-browser-4fi</guid>
      <description>&lt;p&gt;Most of my side-project time goes into a small site that holds the full King James Bible text with a few word games sitting on top of it. The games started as a weekend joke. They are now the part I actually maintain.&lt;/p&gt;

&lt;p&gt;The crossword ate the most evenings, and not for the reason I expected. Drawing the grid is nothing — CSS grid, a couple of input handlers, done in an afternoon. &lt;em&gt;Filling&lt;/em&gt; the grid is where I got humbled.&lt;/p&gt;

&lt;h2&gt;
  
  
  The greedy version, which I shipped anyway
&lt;/h2&gt;

&lt;p&gt;First attempt was about as naive as it gets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sort the words longest first.&lt;/li&gt;
&lt;li&gt;For each word, scan the board for a cell whose letter matches one of its letters.&lt;/li&gt;
&lt;li&gt;Drop it into the first slot that doesn't break a neighbour.&lt;/li&gt;
&lt;li&gt;If nothing fits, shrug and skip the word.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That worked for maybe six words out of ten. The rest got skipped, so the puzzle came out sparse and half the clues pointed at nothing. My fix at the time was to run the whole thing 200 times with a shuffled word order and keep the densest result. It was dumb and it shipped and nobody complained for two months.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually fixed it
&lt;/h2&gt;

&lt;p&gt;Backtracking, but with the cheap heuristics doing the real work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep a &lt;code&gt;Map&lt;/code&gt; of every placed letter keyed by &lt;code&gt;row,col&lt;/code&gt;, so checking a candidate slot is a lookup and not a scan&lt;/li&gt;
&lt;li&gt;score each possible placement by how many &lt;em&gt;existing&lt;/em&gt; letters it crosses, prefer more crossings&lt;/li&gt;
&lt;li&gt;place, recurse, undo on failure, with a step budget so a bad seed can't hang the tab&lt;/li&gt;
&lt;li&gt;if the budget blows, drop the longest unplaced word and restart the branch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The search itself is textbook. The ordering is what moved the numbers. Sorting candidates by crossing count rather then by length cut the average placement attempts by something like 8x on my test set of 40 puzzles.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boring part that mattered most
&lt;/h2&gt;

&lt;p&gt;The word list. Biblical vocabulary is genuinely hostile to crosswords — it is full of proper nouns nobody can spell and long names that cross with nothing. &lt;code&gt;NEBUCHADNEZZAR&lt;/code&gt; is fourteen letters of dead weight. Anything over nine letters now goes into a "bonus" bucket that the generator drops first when it gets stuck, and the core list is hand-checked, 4 to 9 letters, weighted toward words with common vowels.&lt;/p&gt;

&lt;p&gt;I spent two weeks tuning the algorithm and then got a bigger improvement in one evening of editing a text file. That keeps happening to me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seed everything, do it early
&lt;/h2&gt;

&lt;p&gt;The one thing I would do differently: make it deterministic from day one. I now use a small &lt;code&gt;mulberry32&lt;/code&gt; PRNG and pass a seed through the whole generator, so the daily puzzle is identical for every visitor and a bug report is just a seed number. Before that I burned an entire evening chasing a broken grid I could not reproduce, because every reload built a different board.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mobile input
&lt;/h2&gt;

&lt;p&gt;Short version — don't put a real &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; in every cell. One hidden input, a caret you draw yourself, and arrow/tab handling that skips black squares. iOS Safari will fight you about the keyboard staying open; a &lt;code&gt;readonly&lt;/code&gt; toggle on blur is what finally settled it for me.&lt;/p&gt;

&lt;p&gt;The live version is here if you want to poke at the grid behaviour: &lt;a href="https://bible4soul.com/games/bible-crossword/" rel="noopener noreferrer"&gt;the Bible crossword&lt;/a&gt;. The generator runs entirely client-side, so view-source is basically the whole story.&lt;/p&gt;

&lt;p&gt;Curious what other people use for the fill step — I keep reading that a proper CSP solver with arc consistency is the "correct" answer, but for a 13x13 board with a curated list, plain backtracking has been fast enough that I never felt the pain. Has anyone hit a board size where that stops being true?&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
