Gil EskayoAI Engineer · Security · Research

Killer Sudoku

The Killer Sudoku app I wanted: no ads, no nags, a fresh puzzle with exactly one answer every time. Mac, Windows and Linux.

SwiftSwiftUIKotlinCompose Multiplatform
GitHub ↗
The story

I play Killer Sudoku, and the app I used had one habit I couldn't stand: finish placing a digit, and it wiped that digit from pencil marks I hadn't got to yet.

So I built my own over three days in August 2026. Every puzzle is generated on the device and proven to have exactly one solution before you see it. There are no ads, no accounts and no network calls, ever. It ships as a native SwiftUI app for the Mac and a Kotlin port for macOS, Windows and Linux that matches it cell for cell.

01

Why I built it

I wrote one hard rule into the scope before any code: no ads, no in-app purchases, no nags of any kind. The second rule came from that lost-notes moment: your pencil marks are yours, and the app never touches them on its own.

02

What it looks like

A Killer Sudoku board on a dark background: dashed outlines group cells into cages, each with its target sum in the corner; given digits are bold and entered digits lighter.
A Hard puzzle in the macOS build. Dashed outlines are cages with their sum in the corner; bold digits are givens, lighter ones are mine.

It's ordinary Sudoku with one added rule: every dashed cage must add up to the small number in its corner, with no digit repeated inside it. Play is keyboard-first. Type a digit to fill a cell, Shift and a digit for a pencil mark (drawn as a 3x3 mini-grid, 1 top-left, 9 bottom-right), and Cmd+Z to undo. Progress saves continuously, so you can quit mid-puzzle and come back to the same spot.

  • Nothing depends on colour. Cages are dashed outlines and a sum, with no tints, and every state cue (mistakes, selection, completion) uses outline, glow or brightness. It's colour-blind safe by construction.
  • A finished digit never clears your notes. The small 1-9 legend dims a digit once all nine are placed, and your pencil marks stay exactly as you left them.
  • Highlighting never gives hints. Selecting a digit shows where it already is, never where it should go.
  • Four tiers, tracked. Easy, Medium, Hard and Expert, with best time and solve count for each.
03

How it works

There's no bundled puzzle set. When you ask for a puzzle, the generator fills a valid grid, covers it in cages of two to four cells, and a solver checks that exactly one solution exists. If it doesn't, another layout is tried. Only then are the givens revealed: 25 to 40 on Easy, down to 4 to 8 on Expert.

A flow: fill a valid grid, cover it with cages, check for exactly one solution (looping back if not), reveal the givens for the tier, show the puzzle.
From an empty board to a puzzle with one answer.

The Mac app is SwiftUI with a Canvas-drawn board. The Kotlin port uses Compose Multiplatform with its own renderer so it looks the same as the Swift build. Both follow the same 14 written design decisions, ported rather than reinvented, and each has its own test suite.

One set of rules and decisions feeds a Swift core with a SwiftUI macOS app, and a Kotlin port with Compose desktop apps for macOS, Windows and Linux.
One design, two implementations.
04

The hard calls

Difficulty took three tries. First I simulated the solving techniques a puzzle needs, then I measured how hard the solver had to search. Neither separated real generated puzzles: the first graded every one of them hardest. What shipped is simple and predictable. Difficulty is decided at generation time by how many digits are revealed.

Three boxes in order: technique simulation, solver search effort, then the given-density approach marked as the one in use.
The three attempts are written up as decisions 0003, 0007 and 0008.

Speed: profile, then parallelize, then check the build. New puzzles took 10 to 38 seconds. Profiling showed why: 85 to 95% of random cage layouts don't have a unique solution, and each failure still costs a full search. Lowering the search budget made it worse, so instead I ran whole batches of attempts across every CPU core and took the first winner. That cut it to 2 to 4 seconds.

Next I considered rewriting the solver in C++. Before that, I checked what I'd actually been measuring: every number so far came from an unoptimized debug build. The same code in release averaged about 1 second, with 23 of 24 runs under 3 seconds. Not one line of the solver changed. The run script now builds release by default.

05

Where it stands

It runs on my Mac today. The Swift build has 102 automated tests in 14 suites, all passing, and the Kotlin port has 41 more. The project took 46 commits over three days, with the Kotlin port built on the third, and the trade-offs are written down as 14 decision records.

It's distributed locally, not through an app store. To run it on a Mac with Swift 6, clone the repo and run ./scripts/run-app.sh; on any OS with JDK 17 or later, cd desktop-kotlin && ./gradlew run. The decision records explain why it works the way it does.

← More Software Engineering   All projects →