Solving a 2-Minute Puzzle in 2 Months
How a weird little Google puzzle turned into a daily solver, a lot of browser debugging, and a Sokoban-inspired algorithm that made everything much faster.
A while ago, my friend Abdullah introduced me to a game called Tic Tac Go.
The first time I opened it, I had no idea what I was looking at. There was a little O-character, a board full of Xs, and some vague instruction not to make three Xs in a row. It was confusing, kind of adorable, and strangely addictive.
My first puzzle took ten minutes. The next day took five. The day after that took two. Then I got humbled by another five-minute puzzle, which was definitely harder and not a skill issue.
But I kept coming back. I wanted the fastest route, the fewest moves, or, at minimum, a solution better than Abdullah's. Somewhere between all of that, I started wondering how Google made these boards in the first place. How did it place every piece? More importantly, how did it make sure every daily puzzle was actually solvable?
That question sat in my head for a few months.
Then Abdullah asked if I wanted to help build a solver site.
Obviously, I said yes.
The Easy Part: Building a Whole Website
I started with the part that sounded straightforward: a quick Next.js site, a Neon-backed database because I was a student and cheap infrastructure is beautiful, and FastAPI functions because Abdullah's solver experiments were already in Python.
I built a board viewer, solution replays, puzzle titles, past-day browsing, and all of the little UI details that make a project feel like a real thing instead of a terminal command. I moved functions around, tried deployment ideas, wired up GitHub Actions to deploy a Cloud Run service, and kept making the site less ugly one tiny iteration at a time.
The app was starting to look finished.
The solver was not.
The idea had quietly become much bigger than “find a solution.” I wanted a daily job to open Google’s Tic Tac Go page, take a screenshot of the day’s board, understand every piece on it, solve it, and save both the answer and the board history for anyone to replay later.
In other words: a robot that wakes up every day and does a puzzle before anyone does.
Making a Computer Look at a Puzzle
The first problem was getting a reliable screenshot.
It turns out that “open a Google Search page and screenshot the game” is not a single task. It is a collection of tiny tasks that all have opinions about timing, browser binaries, overlays, viewport size, and whether the actual board has decided to load yet.
I tried Playwright locally. Then Browserbase. Then Browserless. The commit history from that weekend is basically a tiny emotional arc: Cmon Playwright, ur a bad boy, Browserbase??, Cmon BrowserBase, and finally, after more retries than I would like to admit, STUPID SCREENSHOT WORKS NOW.
Browserless was the thing that finally stuck. It gave the deployed job a remote browser instead of asking a serverless function to carry around Chromium. Even after that, I had to keep iterating: dismissing the occasional tutorial overlay, waiting for Google’s canvas to settle, scrolling the game into view, trying element screenshots before falling back to a full-page capture, and debugging why the board had decided to sit in the top-left corner of the screen.
I also briefly considered making my own OpenCV parser. It would be free, it would be cool to learn, and it would mean fewer API calls. But the Gemini parser worked really well. For V0, choosing the thing that already worked was the right decision. I kept the OpenCV version around as a future experiment instead of replacing a useful system for the aesthetic satisfaction of replacing it.
The final flow is pleasantly boring now: Vercel Cron triggers the daily endpoint, Browserless captures the live puzzle, Gemini turns the screenshot into an 8×8-ish board representation, the backend runs the selected solver, and Neon stores a solution and replay. Pleasantly boring is usually how you know the debugging worked.
Meanwhile, Abdullah Was Teaching a Computer to Think
While I was fighting browsers, Abdullah was fighting the puzzle itself.
But Abdullah's original solver idea was not ML at all. It was the simplest (and, realistically, the most optimal) answer possible: BFS.
Abdullah and I were taking a Data Structures and Algorithms course at the same time we were getting addicted to Tic Tac Go, so breadth-first search was fresh in our heads. The pitch was simple. Start with the current board, try every legal move, put the resulting boards in a queue, and keep going layer by layer. The first time BFS reaches a winning board, it has found the shortest sequence of moves.
For a small board, that is genuinely a great first solver. It is easy to trust, easy to test, and it taught us the rules of the game in code: what counts as a legal push, when three Xs make you lose, and what an O/O/O win actually looks like.
The problem is that BFS has to remember everything it has seen. Every board configuration needs to live in a visited set so the solver does not loop forever, and every promising configuration waits in the queue until its turn comes up. As boards got larger, the number of possible arrangements of the player, the two O pieces, and all the movable Xs grew ridiculously fast. If the solver also treats every arrow-key walk as a new state, it can spend a lot of memory storing slightly different versions of the same room.
That was the first wall: not just time, but RAM. The queue could get enormous before BFS found a path worth following.
I took that BFS baseline and optimized it into A*, my original “smarter BFS” approach. A* still keeps a frontier of possible boards and a visited record, but it does not explore them in plain first-in-first-out order. It gives each board a score: how much work it has already taken to reach it, plus a heuristic guess of how close it is to a three-O line. Boards that already have useful O placement and fewer blocking Xs get considered earlier.
That helped a lot. My early A*-style solver also collapsed a long walk across empty cells into one move segment before considering the next push. But on larger daily boards, even a well-prioritized search can accumulate a huge frontier. Abdullah's Beam/CNN path was built to guide that search further, and it could be allowed as much as 300 seconds on difficult boards.
Abdullah kept pushing on that question from a lot of angles: reinforcement-learning directions, different training curricula, heuristic search, beam search, and eventually a CNN-guided approach. The working Beam + CNN solver was a huge achievement, especially considering that he didn't have much prior ML experience going in. It gave the project a real baseline and proved that the daily-solver idea was possible.
His full journey is worth its own post, and he wrote it up much better than I could: Finding a 73-Move Solution When Every Correct Move Looks Wrong.
I wanted to take a swing at it too.
My first direction was a linear tree ranker: look at each possible A* branch, describe it with a set of features, and learn which branch looked most promising. It was a neat little regression-style idea, and the checked-in V1 model reached 69.05% holdout top-1 accuracy across its decision groups. That was honestly encouraging for a first attempt.
But it also made me look at the board differently.
Why was I trying so hard to make a model “understand” every move when the game itself had a very recognizable structure?
The answer had been sitting there the whole time.
Tic Tac Go Is Basically Sokoban
Sokoban is an old grid puzzle about a warehouse keeper pushing boxes onto goals. You can walk around freely, but boxes can only be pushed, never pulled. That one restriction makes the game surprisingly brutal: push a box into the wrong corner and it may be stuck forever.
Tic Tac Go has the same energy.
| Sokoban | Tic Tac Go |
|---|---|
| Player | The controllable O |
| Boxes | The other two O pieces |
| Walls | Barriers and board edges |
| Movable obstacles | X pieces |
| Goal squares | Any horizontal or vertical line of three O’s |
There are two important twists. First, the goal is relational: there are no pre-labeled target squares. The puzzle is won whenever the three O’s make a consecutive line. Second, the Xs are not harmless boxes. Push them into three-in-a-row and the puzzle immediately loses.
So a Tic Tac Go state is not just “where am I standing?” It is where the player is, where the two O pieces are, where the X pieces are, which target lines are still possible, and which innocent-looking push has quietly made the board impossible.
That is a Sokoban puzzle. Just a weirder one.
Search the Push, Not the Walk
At first, a solver can search individual keystrokes: up, down, left, right. The problem is that most of those moves do not actually change the puzzle. If the player can walk around the same open region without moving a piece, the solver creates a bunch of different-looking states that all lead to the exact same decision.
The real decision is the next push.
Instead of expanding every key press, the push solver does this:
- Flood-fill the cells the player can currently walk to.
- Find every piece that can legally be pushed from that reachable area.
- Treat each legal push as one search action.
- Repeat.
That small reframing changed everything.
Two player positions in the same reachable room are effectively the same state, so my solver normalizes them into one. It prunes pushes that immediately create three Xs in a row and catches conservative deadlocks, like O pieces pushed into positions that can never contribute to a winning line. It scores possible final O-lines, searches with weighted A*, and only reconstructs the actual arrow-key sequence after finding a push-level solution.
Then it replays the answer independently from a clean board to make sure it really wins and never creates a losing X line along the way.
The key point is that I did not replace A*. A* was my OG approach. I gave it a much better question to answer: instead of “what's the best key to press from this exact square?”, I asked, “which push changes the board in a way that still lets us win?”
Iterating Until It Stopped Taking Five Minutes
The first push solver was already useful. It searched at the right level, found valid paths, and solved 18 of the original 20 testing boards. But the hard boards were still hard. A few particularly evil puzzles had huge stretches where moving Xs looked equally bad in every direction, and the search would spend a long time exploring them.
So I kept tightening it. Here is what that actually meant.
Caching the boring facts. A search reaches the same kind of question over and over: “where can the player walk if these pieces have not moved?”, “is this O trapped?”, and “which three-cell lines can still become a win?” Each answer requires work; usually a flood-fill over the board or a scan of candidate lines. Instead of recalculating that work every time a search branch revisited a state, I memoized it. Same state, same answer, no extra thinking. It is not glamorous, but it lets the solver spend its time on new boards instead of rediscovering the same hallway.
Deadlocks and losing pushes. Some bad moves are easy to prove bad. If an X push creates a horizontal or vertical X/X/X, the game is already lost, so the solver drops that branch immediately. Some O positions are dead too: an O can be pushed into a spot where walls make it impossible for that piece to ever reach any candidate three-O line. I precompute safe versions of those checks and prune only when I can prove a branch cannot work. That “prove it first” part matters. An overly aggressive deadlock detector can accidentally throw away the one weird solution the board needed.
Macro pushes. Sometimes a piece is in a one-cell-wide corridor and there is only one sensible thing to do: keep pushing it in the same direction until it reaches the end or a real choice appears. A macro lets the search treat that forced sequence as one higher-level option instead of making it rediscover the same obvious push three or four times. It is an optimization, not a shortcut around the rules. Internally, every macro is still stored as ordinary pushes, then expanded back out before I return a solution.
More than one way to prioritize the search. Weighted A* has a dial: care more about moves already spent, or care more about the heuristic's guess of a promising board. One setting can be great at direct puzzles and terrible when the solution needs a temporarily ugly X-clearing detour. So my solver runs a small portfolio of search schedules—weighted, greedier, line-focused, and recovery-oriented—over the same legal push generator. I stopped asking one priority formula to be right about every puzzle.
Committed, bounded beam searches. A final Tic Tac Go win must occupy one specific horizontal or vertical triple. For a hard board, the solver can enumerate plausible assignments: which two cells should hold the O pieces, and which remaining cell should the player reach? A committed beam search temporarily picks one of those plans and keeps only a bounded number of the most promising partial boards at each depth. That focus helps when a general search is stuck in a giant plateau of equally mediocre X moves. It still checks transpositions, adds novelty penalties so it does not loop, and falls back to other plans rather than pretending its first guess is always right.
A learned ranker with its hands tied. The later ranker looks at already-legal candidate pushes and scores details like whether a push improves a possible winning line, moves an O versus an X, creates extra X threats, or matches patterns from verified solutions. It does not generate moves. It does not declare a state legal. It does not prune a branch because a model has a feeling. It just says, “of the moves the classical solver has already proven legal, maybe try this one earlier.”
That safety boundary mattered to me. The model can say, “try this legal push earlier.” It cannot say, “trust me, this impossible move is fine.” Every returned answer is expanded into real U/D/L/R keystrokes and replayed from the original board. The verifier checks the same rules a player would: no walking through barriers or pieces, no illegal pushes, no intermediate X/X/X loss, and an actual O/O/O line at the end.
That speed difference is the part that still feels a little silly. The puzzle did not get smaller. The computer just stopped spending most of its time considering different ways to walk around the same room.
Conclusion
I started this project because I wanted to beat a two-minute puzzle a little faster than my friend.
Then it became a website, a browser automation project, a vision pipeline, a database, a cloud deployment, an ML experiment, and eventually a very specific lesson about problem framing.
Sometimes the answer is not a bigger model. Sometimes the answer is noticing what kind of problem you are actually holding.
Tic Tac Go looked like a weird little Google game. It turned out to be a Sokoban-style search problem hiding behind some flowers and rounded buttons.
And, somehow, that was enough to turn a two-minute puzzle into two months of work.
I would absolutely do it again.
- Shaurya Verma
Abdullah’s write-up goes much deeper into the original Beam + CNN solver: read it here.