Sqrubs
A cross-platform Scrabble analysis board that reads a position off a screenshot or a photo, finds every legal move on it and ranks them — built to show you what you missed, not to play your turn for you.
Not deployed — the write-up is the artefact.
The problem
The move you played is not the move that was there.
You finish a game, you lost by nine points, and somewhere around turn six there was a bingo sitting on your rack that nobody at the table saw. Scrabble is one of the few games where the best move is a fact — a finite board, a finite rack, a fixed word list — and where a human being reliably cannot find it. You can review a chess game afterwards and be told exactly where you went wrong. There is no equivalent for the board you just packed away.
Sqrubs is that review. You set up the position, you enter the rack, and it returns every legal move ranked by score. The distinction it is built around is that it is an analysis board, not an assistant: it is for the game that is over, the way a chess engine is for the game that is over. A solver you consult mid-turn against someone sitting opposite you is a different product with the same code in it, and I did not want to build that one.
What I chose
Flutter, targeting Android, iOS, macOS, Windows and web from one codebase, with Riverpod holding board, rack, dictionary and solver state — chosen because its providers are testable without a widget tree, and most of this project is logic rather than screens.
The dictionary is a bundled asset — nothing leaves the device and none of it needs a network — read into a trie at launch. The solver is anchor-based cross-check move generation, the Appel and Jacobson approach: find the empty squares adjacent to played tiles, work out for each of them which letters could legally form a cross-word, build prefixes leftward out of the rack, then extend rightward across the anchor while the trie says a word can still continue. Then the same thing again vertically. Scoring applies multipliers only to newly placed tiles, adds every cross-word the placement creates, and adds fifty if the rack empties.
What I rejected
Permutations of the rack. The obvious first solver: generate every arrangement of up to seven tiles, try it at every position, keep what the dictionary accepts. It is correct, it is about fifteen lines, and it does an enormous amount of work to discover that no English word starts with "qz". The trie is what turns that into pruning — the moment no word continues a prefix, the whole branch is gone — and the anchor pass is what stops it from considering positions that could not be legal placements in the first place.
A hash set of words. A set answers "is this a word" in constant time and answers nothing else, and move generation is not a membership question. Every step of it asks whether a prefix can still go somewhere, which is the one question a set cannot answer and the only question a trie is for. The trie costs more memory for the same word list and that is the entire trade.
A GADDAG. The serious answer, and what a competitive engine uses: storing every rotation of every word so extension runs both directions from an anchor without the separate left-part pass. It is roughly five times the memory of a plain trie, in exchange for speed on a problem where the user is a person waiting for a list. I would rather spend a second than five times the footprint on every phone the app runs on.
Solving on the main thread. The move generation runs in an isolate. A solve that janks the UI reads as a broken app rather than a busy one.
What it cost
Entering a position was the slow part, and the solve is instant. A mid-game board is thirty-odd taps through a per-cell dialog before you get an answer, which is tolerable when you are reviewing one interesting position and tedious the moment you want to review a whole game. The fix was always photographing the board, and the first version of this page called that a different project. It became the next one.
You give Sqrubs a screenshot or a photograph — from the camera, from the gallery, or shared straight out of another app on Android — and it reads the board and the rack off it. A screenshot finds its own corners; a photo gets four handles to drag, with a magnifier and the fifteen-by-fifteen grid drawn live over it. The picture is warped square and then snapped to the grid lines it actually has, so a corner a third of a cell out still lands. Which cells hold tiles is decided from pixels before anything is read: a tile is a letter shape on a tile-coloured square, which is what stops the "TW" printed on a bonus square from coming in as two letters. ML Kit's text recognition reads the letters, on the phone, so the photograph goes nowhere either; it re-reads the doubtful ones on a contrast-normalised sheet, and hands whatever it still cannot read to a shape match against letters rendered from a sans and a serif font.
What I wrote before was right about the danger: one misread cell produces a confident wrong answer. So the reader does not trust itself. Every word it finds, across and down, goes through the dictionary the solver uses, with tile counts and point values checked beside it, and an invalid word may have up to three of its unsure letters varied together until it is a word. A letter read with confidence is never changed, a guess is only filled in when it is the one letter that fits, and when most of the words fail the reader stops correcting and suggests the board is in the other language. Whatever is still doubtful comes up outlined for a tap, with the photograph a button away. On twelve test boards — six synthetic, four screenshots from other Scrabble apps and two photos of a real board — it reads 96.5% of letters raw and 98% after validation, 175 of 177 on the real screenshots, in one to three seconds.
It is not free. The reader runs on Android and iOS only: ML Kit has no desktop or web build, so on three of the five platforms the button is not there and a position is still thirty taps. A photo taken at a steep angle shifts raised tiles out of their cells by parallax, and a physical blank shows no letter, so it is inferred only when one letter fits. And it moves a line this page draws. A screenshot of an online game, rack included, is exactly the position you would want mid-turn, so whether Sqrubs is an analysis board or an assistant is now decided by when you use it rather than by what it can do. I built it anyway, because the review it is for is the one that needs a whole game entered, and nobody does that by hand.
Adding French was not a translation job. The user interface strings are a file; the game is not. French Scrabble has its own letter distribution, its own point values and its own treatment of accents, which meant the tile model itself had to become language-dependent rather than a constant map. That is the kind of thing you find out in phase ten that you should have known in phase one.
Blank tiles cost more than anything else of their size: each one multiplies the search by twenty-six and scores as nothing.
The outcome
It works, on the plan's ten phases, on five platforms, in two languages, with tests behind each phase, and it reads a board off a photograph now. It is not shipped yet, but the distance between those two words has changed. What stood between them was never code: an icon, a privacy answer, and a word list whose licence I can point at — the well-known tournament lists are not the public-domain files they are assumed to be. The icon is drawn, the privacy policy is published, and the English list is ENABLE with the Wordnik list beside it, both of which say what they are. What is left is Google's review of the Play Store listing. When it is through, this paragraph gets a link instead.
