<?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: Quinn Harlow</title>
    <description>The latest articles on DEV Community by Quinn Harlow (@quinnharlow).</description>
    <link>https://dev.to/quinnharlow</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%2F4230573%2F9f0a3383-999d-4422-b026-d113aea1cf53.png</url>
      <title>DEV Community: Quinn Harlow</title>
      <link>https://dev.to/quinnharlow</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/quinnharlow"/>
    <language>en</language>
    <item>
      <title>How I generate Zip path puzzles that have exactly one solution</title>
      <dc:creator>Quinn Harlow</dc:creator>
      <pubDate>Fri, 06 Nov 2026 14:52:17 +0000</pubDate>
      <link>https://dev.to/quinnharlow/how-i-generate-zip-path-puzzles-that-have-exactly-one-solution-jmg</link>
      <guid>https://dev.to/quinnharlow/how-i-generate-zip-path-puzzles-that-have-exactly-one-solution-jmg</guid>
      <description>&lt;p&gt;LinkedIn's daily &lt;strong&gt;Zip&lt;/strong&gt; puzzle is simple to explain: draw one line through every cell of a grid, passing the numbered cells in order (1, 2, 3 …), never crossing a wall. Underneath, it's a classic problem: finding a &lt;em&gt;Hamiltonian path&lt;/em&gt; that visits every cell exactly once.&lt;/p&gt;

&lt;p&gt;You only get one official puzzle a day, so I built a free &lt;strong&gt;&lt;a href="https://puzzleanswertoday.com/zip-unlimited/" rel="noopener noreferrer"&gt;Zip Unlimited&lt;/a&gt;&lt;/strong&gt; with 480 boards from 5×5 to 8×8, plus a &lt;strong&gt;&lt;a href="https://puzzleanswertoday.com/zip-unlimited/daily/" rel="noopener noreferrer"&gt;daily board&lt;/a&gt;&lt;/strong&gt; that's the same for everyone. The hard part wasn't drawing lines. It was making sure &lt;strong&gt;every board has exactly one solution&lt;/strong&gt;, because a puzzle with two answers feels broken. Here's how the generator works.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Start from a random path, not from the numbers
&lt;/h2&gt;

&lt;p&gt;It's tempting to scatter numbers on a grid and hope a path exists. That fails constantly. Instead, I generate the &lt;em&gt;answer&lt;/em&gt; first: a random Hamiltonian path over the whole grid.&lt;/p&gt;

&lt;p&gt;The trick is the &lt;strong&gt;backbite move&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with a boring snake path (left to right, then right to left, row by row).&lt;/li&gt;
&lt;li&gt;Pick one end of the path (flip a coin to choose which end).&lt;/li&gt;
&lt;li&gt;Pick a random grid neighbour &lt;code&gt;x&lt;/code&gt; of that end.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;x&lt;/code&gt; isn't the cell just before the end, connect the end to &lt;code&gt;x&lt;/code&gt; and &lt;strong&gt;reverse the part of the path after &lt;code&gt;x&lt;/code&gt;&lt;/strong&gt;. The result is still a path through every cell.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Repeat that a few thousand times (I use &lt;code&gt;cells × 40&lt;/code&gt; attempts) and the snake turns into a well-mixed random path. It's cheap and always valid.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;backbite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;nbrs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;dr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;dr&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;dc&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nbrs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:][::&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Put numbers on the path
&lt;/h2&gt;

&lt;p&gt;Cell 1 is the start of the path and the last number is its end. A few more numbers go on random positions along the path, more on bigger boards (5–7 numbers on 5×5, up to 8–13 on 8×8). Because they are numbered in path order, the puzzle is solvable by construction.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Prove it has only one solution
&lt;/h2&gt;

&lt;p&gt;Now the important part. I wrote a solver that searches for solutions and &lt;strong&gt;stops as soon as it finds two&lt;/strong&gt;: one is fine, two means the board is ambiguous.&lt;/p&gt;

&lt;p&gt;A plain depth-first search is far too slow on 8×8, so it prunes hard. Cells are bits in a Python integer, which makes the checks fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Number order:&lt;/strong&gt; you can only step onto a numbered cell if it's the next number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The end cell comes last:&lt;/strong&gt; stepping onto the last number with cells left over is a dead end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Degree check:&lt;/strong&gt; every unvisited cell needs at least two open neighbours (one if it's the end cell). Otherwise it can never be passed through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connectivity:&lt;/strong&gt; a flood fill from the current head must reach every unvisited cell. If the line has cut the board in two, stop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last check alone removes most dead branches, because a human solver uses the same rule ("don't split the board").&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Fix ambiguity by looking where the answers split
&lt;/h2&gt;

&lt;p&gt;If the solver finds a second solution, I compare it with the intended path and find &lt;strong&gt;the first cell where they go different ways&lt;/strong&gt;. Then I add a constraint right there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;either a &lt;strong&gt;wall&lt;/strong&gt; between the two cells the other solution uses (if that link isn't part of the intended path),&lt;/li&gt;
&lt;li&gt;or a &lt;strong&gt;new number&lt;/strong&gt; on the intended path at that position.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then solve again. Most boards become unique after a few rounds. Any board that still isn't unique after 60 rounds, or that takes the solver too long, is thrown away.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. How fast is it?
&lt;/h2&gt;

&lt;p&gt;Measured on a laptop (Python, single core):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Kept / tried&lt;/th&gt;
&lt;th&gt;Time per kept board&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5×5&lt;/td&gt;
&lt;td&gt;8 / 15&lt;/td&gt;
&lt;td&gt;0.01 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;8 / 13&lt;/td&gt;
&lt;td&gt;0.03 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7×7&lt;/td&gt;
&lt;td&gt;8 / 26&lt;/td&gt;
&lt;td&gt;0.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;8 / 40&lt;/td&gt;
&lt;td&gt;11 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;8×8 is where the search gets expensive, which is why the boards are generated ahead of time and shipped as one small JSON file (about 80 KB for all 480). The browser never has to solve anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The front end
&lt;/h2&gt;

&lt;p&gt;The game itself is vanilla JavaScript drawing an SVG grid. A few details that made it feel good on phones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pointer events with &lt;code&gt;touch-action: none&lt;/code&gt; on the board, so dragging doesn't scroll the page.&lt;/li&gt;
&lt;li&gt;Dragging back over your line undoes it; tapping a cell on the line cuts it there.&lt;/li&gt;
&lt;li&gt;If you swipe quickly along a row, it fills every cell up to your finger, so fast swipes don't skip cells.&lt;/li&gt;
&lt;li&gt;The daily board is chosen in the browser: a fixed shuffle of the boards (seeded PRNG), walked one step per day, so everyone gets the same puzzle without a server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The boards from this generator are playable at &lt;a href="https://puzzleanswertoday.com/zip-unlimited/" rel="noopener noreferrer"&gt;Zip Unlimited&lt;/a&gt;. They're my own puzzles in the same style, not LinkedIn's, and the site isn't affiliated with LinkedIn.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was written with the help of AI, and the project was built with an AI coding assistant. The algorithms and timings above are taken from the actual code and measurements.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>python</category>
      <category>gamedev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
