Long Live AI Slop

Page content

I’ve been “busy” fiddling around with agentic coding using the Pi agent. This time around, it’s to see what the real limits of a local agent and model truly are. Why? I ran out of Claude tokens on an unrelated project, and that made me wonder just how far I could get without using it in a pinch.

As it turns out… local models are pretty limited, but functional with enough planning.

On Queens

LinkedIn has a daily puzzle game they’ve called Queens. It’s a variant of the N-Queens design where they’ve removed full diagonal exclusion. So the rules work like this:

  1. One queen per color.
  2. One queen per row or column.
  3. Queens can’t touch.

Simple and easy to pick up. Color regions are designed in such a way that they guide cell elimination until it’s possible to place a queen. I saw this as a great opportunity to see how far AI slop could get me in designing a locally playable version. Rather than just graphics or enemy decision trees, it’s governed by logic, something LLMs are notoriously bad at—especially local ones.

All Roadblocks, All the Time

My first problem was the fact that local models are insanely idiotic. They’re great at pattern reproduction, so they can spew out usable code on a number of topics. Most even understood the N-Queens underpinnings. My first attempt was essentially to ask for an HTML playable version of N-Queens with the LinkedIn rule modification. I had the presence of mind to split it up into three segments:

  1. Generation
  2. Validation
  3. Rendering

That allowed for a minimal API and separation of concerns, with the potential for tests of all components.

And of course, that ended in utter disaster. It didn’t matter if I used the dense Qwen3.6-27B, the more agile Qwen3.6-35B-A3B MoE, Gemma-4-26B-A4B, Gemma-4-31B, or any of their numerous derivatives. They were able to generate puzzles, but the puzzles often had multiple solutions, featured copious deadly patterns, and were otherwise unsolvable using pure deduction based on the region patterns.

I always tried telling the model what the problem was, and they confidently stated they “finally removed all chances of a deadly pattern” every time. No attempt ever worked; it was always a lie or complete misjudgement. The model had no idea how to truly prevent puzzles that weren’t solvable. The end results always looked possible, but a few eliminations tended to reveal the truth.

Solver Ahoy!

So I did what any normal person would do: I added a puzzle solver to the validation logic. Any generated puzzle must pass the solver before being presented to the user. For the solver, I designed a set of heuristics I commonly used while solving easy to advanced puzzles, and defined them using simple language the LLM could implement.

It took a few tries, but the solver eventually got there. The first few iterations were ridiculously inefficient, of course. I spent several rounds purely telling it to add fast exit paths and various short-circuits so it didn’t spend time trying to solve impossible boards. That also meant several cycles revising the generator, surprisingly. I went from BFS generation to Voronoi partitions, and then back to BFS. The problem with Voronoi is that it was too random and caused ceaseless puzzle rejections. The BFS could be guided, bootstrapped with anchor sections that lead to early meaningful eliminations, and so on.

One particular refinement was to avoid simply discarding puzzles on the first failure. Instead, I added a mini-profiler to see where the solver stalled. Then the board went back into the generator for refinements—shrinking too-large regions and random mutations along boundaries before trying again. That reduced the amount of rejected puzzles and therefore perceived generation time to the player.

But even this had a fatal flaw. One of the advanced techniques implemented in the solver is forcing-chains. The idea is to clone the board, add a queen to a likely cell, and follow using standard elimination techniques until there’s a contradiction or solution. Simple, right?

Unfortunately a solution doesn’t mean the solution. With no contradictions, the queen was being allowed in the wrong location, and other placements also succeeded where they shouldn’t, and you’d end up with a board that has multiple solutions. This usually appeared as a deadly pattern consisting of two columns that each shared two rows of possible candidates where either combination could solve.

This was my White Whale bug. Any time forcing chains were required to solve a puzzle, the puzzle had a high chance of having multiple solutions. No LLM could track this down or solve it. I tried for days using every prompt, explanation, and revision to plug this hole, and everything failed.

Eventually I realized it was never actually comparing the solver eliminations to the puzzle key, and I nearly lost my shit. Yes, this was a stupendously idiotic bug, but it also meant it was legitimately possible to solve the puzzle multiple ways. So in went another gate that, if the final solution didn’t match the key, the puzzle required mutation or rejection. Eventually that grew into a uniqueness enforcement step. I also told it to only accept eliminations through contradictions, because those are safe and can’t lead to a false solution. If the solver genuinely got stuck at this point by making no progress after applying all techniques twice in a row, the puzzle couldn’t be solved.

That finally did it. It was slow as hell to generate a working puzzle at higher board sizes (especially above 9x9), but I couldn’t get it to generate invalid boards anymore. The game was finally playable on hard difficulty.

A frontier model may have been able to figure all of this out on its own, but a local one definitely couldn’t. They’re task-rabbits, at best.

Final Tweaks

During all of this, I was also working in various usability enhancements:

  • A solution overlay if you lose.
  • A timer with pause/resume that obscures the board.
  • Click and drag to toggle cells.
  • A button to clear the board of all marks.
  • Improved “graphics”.

Yes, it’s AI slop, but I wanted it to at least be pretty AI slop. Usable AI slop.

I’d still like to get some more optimizations into board generation. I assume there are ways to invert the solver so it can be used to influence or drive board generation itself. That would dramatically reduce, or even entirely eliminate, board rejection. Human puzzle authors use this kind of technique while building tricky or creative boards, so there must be a way to automate it. I obviously didn’t put a lot of thought into this whole endeavor, so I’m not sure I want to spend any more time forcing that kind of ability into the system. Maybe someone else will feel so inclined.

In any case, the full game is on my site here, and the code is as Queendom on GitHub. If nothing else, now anyone can enjoy infinite 6x6 to 12x12 puzzles of varying difficulty.

Shut Your Pi Hole

I mentioned the Pi agent in the beginning. It’s possible to use Claude Code with local models by pointing ANTHROPIC_BASE_URL at the model server. Whether you use Ollama, LM-Studio, llama.cpp, or vLLM, it seems to work with all of them. Unfortunately it’s designed to work with its own models with much larger context windows, and the system prompts reflect that.

Even with context cranked all the way up to 256k, Claude Code was eating my context like a limitless buffet. Pi on the other hand, is probably the most minimalist agent there is. It’s so stripped down that it has no sub-agents, no web fetching tools, and the default prompt is tiny. That’s an immediate context savings of 20-30k. This lets planning much more viable, even for more capable models with larger memory demands which leave less room for context.

That also introduced me to the Pi package repository where I discovered pi-tasks. It forces a pipeline workflow as a task list with multiple implementation phases. It asks clarifying questions. It produces a step-by-step plan, checklists, captures test output, validates, and so on. That’s something I’ve never even seen Claude do except through outright coercion.

Of course, this is necessarily limited by model capability. The first time I tried it on Queendom, it fork-bombed my system with npm test threads because I’m stupid and didn’t resource-cap the Docker container it was running in. But it should have never been running those tests in the first place. In an optimization run, you’re supposed to find one seed that exercises all of the paths in the current generation stack on a single reasonable board size and difficulty (10x10 hard is good). Then use that same seed to test before and after revisions. Instead, it was running the profiler it built in parallel on every board size and difficulty combination. Not only was this slow, it ran my system out of RAM and caused a litany of OOM terminations before everything seized and I had to force-reboot.

I suppose Claude Opus may have done that too, but I doubt it. The judgment of local models is questionable at best. I simply don’t trust them to sit in the driver’s seat unattended just yet. Even a simple task like, “Please find possible optimization paths for generating hard difficulty game boards at 10x10 and higher,” can go hilariously awry. Then again, I’ve had to tell Opus 4.8 not to run the entire test suite for a project any time it touches a single piece of code. No, I don’t need to wait for a 10-minute end-to-end series of tests because you rewrote a single function in one module, you maniac.

And that’s the real area where agents and models have the most room for improvement: common sense. Experienced devs have several shortcuts and best practices they employ proliferously. Models seem to adhere to some of these while mysteriously neglecting others. So the rule is always: work with a model to build a plan, then go over the plan with a fine-toothed comb before transforming it into a checklist of concrete tasks. Then revise the tasks. At least for now, any relationship with an agentic system must be adversarial.

Like any junior dev, they simply can’t be trusted. At least, not yet.

Until Tomorrow,