What If a Model's World Was a Program?
Some decisions are reflexes and some need deliberation. A small experiment in learning a world model as code: synthesize a program from observations, verify it against everything seen, and plan inside it before acting.
Is Text Just a Slow Way to Think?
I’ve been stuck on a question: is text just a slow way for a model to think?
Large language models do everything through language. Ask one to route a request, approve an action, or decide what happens next, and it writes out its reasoning token by token before committing to an answer. For many decisions that seems wasteful. We don’t narrate our way through catching a ball. Nobody taught me to flinch; I just do.
That question took me through decision models, reflexes, and world models, and ended in a small experiment I’m calling chessworld: an agent that learns the rules of chess by playing, writes them down as a program, and calls in an LLM when the program can’t explain what it sees. This post is the path and what I found.
Text Is Sometimes the Thinking
The first thing I had to accept: for a transformer, generated text isn’t only a slow readout of thinking that already happened. Each forward pass does a fixed amount of computation, so writing out intermediate steps is how the model gets more room to think. Squash a multi-step problem into one pass and you don’t get a faster answer. You get a worse one.
But many decisions aren’t multi-step problems. They’re choices from a short list. That’s the bet behind decision models like TypeSafe’s Jev [1], which reads the program’s state and a set of typed questions, then returns structured decisions with calibrated probabilities in one parallel pass instead of generating text. It’s fast because it doesn’t reason. That’s the point, and also the limit.
So the honest answer isn’t “text bad, actions good.” It’s that some decisions are reflexes and some need deliberation, and a good system should know which is which.
Reflexes Are Learned Too
The flinch analogy turned out to be more interesting than I expected. A reflex isn’t knowledge from nowhere. Evolution trained it over millions of years and built it into your wiring. In machine learning terms, evolution is the pretraining and the reflex is a frozen, very narrow policy.
What makes animals capable isn’t only reflexes, though. A squirrel planning a jump between branches is reasoning without words. It has an internal model of how the world responds to its actions, and it runs that model forward before it commits.
That’s the idea behind world models. MuZero [2] learns a compressed model of its environment and plans by imagining action sequences, never producing a sentence.
The Problem With Neural World Models
A neural world model predicts the next state, but you can’t easily inspect why it predicted it, or check that the prediction obeys any rules. If it says a bishop jumped over a pawn, nothing stops it.
That’s where my idea came in: what if the world model were code?
A program can be run, tested, and checked. If the model writes a program for how its world works, every prediction becomes testable. You can assert that bishops never jump and that pieces don’t vanish. You can replay everything you’ve observed and see exactly where the program disagrees with reality.
Code is also compositional. A single “slide until something is in the way” rule covers the rook, the bishop, and the queen. Write it once and it works for all three, and for any new piece that moves the same way. Neural models are notoriously bad at that kind of reuse.
I’m not the first to think this. DreamCoder [3] solves problems by writing programs and gradually builds its own library of reusable concepts. WorldCoder [4] and Code World Models [5] have LLMs write Python simulators of an environment and fix them when predictions fail. Josh Tenenbaum’s “game engine in the head” [6] proposes that humans understand physics by running something like a simulation in their minds. But I wanted to build it myself to see how it feels.
The Hybrid
The architecture I settled on has two parts, a fast one and a slow one, borrowing Kahneman’s terms [7].
As an aside, I’m not the only one borrowing them. TypeSafe calls Jev a “System One” model after the same fast and slow split [1].
System 1 is a small library of rules. Think of each rule as a page that might be in the world’s rulebook: pawns can move two squares on their first move, castling moves the rook too, a pawn on the last rank becomes a queen. Pick any combination of pages and you get a complete rulebook, written as code the agent can run. With only a handful of pages there aren’t many combinations, so the agent can simply try them all. That’s fast and cheap.
System 2 is an LLM that writes new pages. When no rulebook in the library explains what the agent sees, it asks the LLM. The LLM gets the exact thing that went wrong and writes code for the missing rule. The important part: that code isn’t trusted. It’s checked against everything the agent has seen, and if it gets anything wrong, that mistake goes straight back to the LLM.
The LLM proposes. The data decides.
One thing to keep in mind from the start: an LLM has already read a lot about the world, chess included. When it supplies a missing rule, it may be remembering rather than discovering. That’s part of why it’s useful, and I’ll be upfront about where it happens.
Put together, it’s a loop: watch what happens, find a rulebook that explains it, check that rulebook against everything seen so far, and when the check fails, use the exact failure to fix it. Once the agent has a rulebook it trusts, it can plan by imagining moves inside that rulebook before making a single real one.
In the code, that’s four pieces:
| Piece | What it does |
|---|---|
| Rule library (System 1) | A handful of possible rules. Every combination is a complete rulebook the agent can run. |
| Curious learner | Keeps every rulebook that hasn’t been wrong yet, and goes looking for moves where they disagree. |
| LLM (System 2) | When every rulebook has been wrong, writes the missing rule as code. That code is checked against every move seen so far. |
| Planner | Thinks a few moves ahead inside the learned rulebook, then plays for real. |
The Experiment
I picked chess because most people know the rules, or think they do.
Picture someone who has never seen chess, sitting in front of a chess app. They can’t read the rulebook. All they can do is move pieces and watch what the app allows. That’s my agent. The real rules live in a file it never reads. It plays both sides, and after every move it gets four things:
That’s all it ever sees: where the pieces were, which moves the app allowed, which move was played, and where the pieces ended up. It plays lots of short games, up to eight moves each, every one starting from a random board with two kings and a few other pieces.
Rulebooks as Programs
The agent doesn’t start from nothing. It knows the day-one basics: how each piece moves, that landing on a piece captures it, and that you can’t leave your own king in check.
What it doesn’t know is the fine print. I gave it six possible extra rules. Four are real chess. Two are things beginners often get wrong:
Each extra rule is either on or off. Six on/off switches make 64 combinations, so the agent starts with 64 possible rulebooks. Each one is a small program that answers two questions: “which moves are allowed here?” and “what does the board look like after this move?” Here’s the one that matches real chess:
WorldModel(
pawn_double_step=True,
castling=True,
promotion=True,
en_passant=True)
Learning by Throwing Rulebooks Away
Learning is one simple rule: after every move, ask each rulebook what it expected. If it got anything wrong, throw it away.
survivors = [
m for m in survivors
if m.legal_moves(s) == legal
and m.step(s, mv) == nxt]
That’s the whole thing. No training, no gradients. Every move is a little quiz, and all the remaining rulebooks take it at once. Here’s one quiz, a pawn reaching the far end of the board:
Every rulebook without promotion predicted a pawn on e8. A queen showed up instead, so all of them are out in one move. Nobody had to explain why they were wrong. They predicted a game that didn’t happen.
When only one rulebook is left, the agent has learned chess. Or at least, all the chess its library can describe.
Following a Run
A run is one complete attempt at learning: start with all 64 rulebooks, play until only one is left, and count the moves it took. The boards are random, so some runs get lucky and some don’t. I did 20 runs of everything. But the clearest way to see what’s going on is to follow one run closely, first with an agent that plays random moves, then with one that’s curious.
Run 1, Playing at Random
- Move 1. The very first list of legal moves never offers a pawn capturing straight ahead. Half the rulebooks are gone: 32 left.
- Move 2. The next list includes castling and a pawn moving two squares. 8 left.
- Moves 3 to 65. Nothing. The agent shuffles pieces around, and the 8 remaining rulebooks agree about every move.
- Move 66. A king in a spot where castling would go through or out of check, and the app doesn’t allow it. 4 left.
- Move 84. En passant finally comes up. 2 left.
- Move 159. A pawn reaches the last rank and turns into a queen. 1 left. Done.
Most of that run was wasted. The situations that matter, a pawn about to promote or a pawn landing right beside an enemy pawn, rarely happen by accident. In random play en passant shows up about once every 300 moves.
Run 1, Playing Curious
So I made the agent curious. Before every move, it imagines the next couple of moves with each rulebook still standing and looks for a spot where they disagree. Then it goes there on purpose. Here’s the kind of thing it sets up:
Same run, same starting board, curious agent:
- Move 1. The same first list of legal moves. 32 left.
- Move 2. Black castles. 8 left.
- Move 4. It walks a pawn onto the last rank, and it becomes a queen. 4 left.
- Moves 5 to 7. It pushes a white pawn up to f5, plays a black pawn two squares to land right beside it, then captures it en passant. 2 left.
- Moves 19 and 20. It gives check with a rook, and the app won’t let Black castle its way out. 1 left. Done.
Twenty moves instead of 159.
All 20 Runs
One run can be a fluke, so here are all of them:
Playing at random took a median of 322 moves, and one run needed 873. The curious agent took about 30, and never more than 83. That’s roughly ten times faster, just from going where the rulebooks disagree.
An agent that knows what it doesn’t know, and goes looking, learns much faster.
When the Rulebook Is Missing a Page
Plenty of casual players have never heard of en passant. So I ran everything again with en passant taken out of the library. Now there are only 32 rulebooks, and none of them is right. It’s an agent that learned chess from casual players.
Here’s Run 1 again under those conditions:
- By move 20, the curious agent is down to one rulebook: pawns can move two squares, castling, promotion, and no beginner myths. As far as it can tell, it knows chess.
- Moves 21 to 81. It keeps playing happily. With only one rulebook left there is nothing to disagree about, so curiosity has nothing to chase.
- Move 82. The app offers a move the last rulebook says is impossible. Zero left.
That’s a lesson in itself. Curiosity only works on questions you know how to ask. Across 20 runs the pile emptied after a median of 286 moves, barely faster than random play at 322. The agent couldn’t go looking for a rule it had no word for. It had to wait for the world to surprise it.
Here’s the surprise from Run 1:
Black has just pushed a pawn from f7 to f5, right beside White’s pawn on g5. The app says White may now play g5 to f6. The agent’s rulebook says that’s impossible: f6 is empty, and pawns only move diagonally when they capture something.
This is the moment the whole setup is built for. The agent isn’t confused in some vague way. It can point at the exact position, the exact move, and the exact prediction that broke. That’s what it hands to the LLM.
Asking the LLM for the Missing Page
The LLM Proposes
The LLM gets three things: the agent’s best rulebook, the position, and the one move it can’t explain. Its job is to write the missing page: two small functions, one that says which moves are allowed and one that says what happens after a move. They sit on top of the existing rulebook. The LLM doesn’t rewrite chess. It adds the one rule that’s missing.
I gave the exact prompt to a fresh Claude instance with no other context. Condensed, the heart of what came back looks like this:
def step(s, move, base):
a, z = move
nxt = base.step(s, move)
is_ep = (
s.board[a] in "Pp"
and a % 8 != z % 8
and s.board[z] == EMPTY)
if is_ep:
# remove the passed pawn
cap = a - a % 8 + z % 8
b = list(nxt.board)
b[cap] = EMPTY
nxt = replace(
nxt, board=tuple(b))
return nxt
In plain words: if a pawn moves diagonally onto an empty square, remove the enemy pawn it just passed. The other function adds the move itself to the list of allowed moves, but only right after an enemy pawn jumps two squares to land beside one of yours, and only if it doesn’t leave your own king in check.
The variable name gives it away: is_ep. The LLM didn’t work out en passant from one example. It recognized it. That’s exactly what the LLM is there for, bringing knowledge the library doesn’t have. But it’s worth being honest that chess is something it has read a lot about.
The Data Decides
Recognizing a rule isn’t the same as getting it right. So the new page gets the same quiz as every rulebook: replay all 82 moves the agent has seen and check every prediction. It passed all 82 on the first try.
I repeated this with four other runs. Each one hit its surprise in a different position, somewhere between move 227 and move 741, and each time the LLM’s first answer passed every move. Then I did something the agent can’t do, and checked all five answers against the real rules directly: 33,757 positions, 401 of them with en passant available, including the sneaky case where capturing en passant would expose your own king. All five got every one right.
To make sure the checking really works, I also fed it a deliberately broken answer, one that removes the captured pawn but never allows the move. It was thrown out at move 82, the exact position it couldn’t explain, and that became the next thing to send back to the LLM.
Here’s how the pile of rulebooks looks across the three situations:
Planning With the New Rule
A rulebook is only worth learning if you can use it. So I gave the agent a puzzle. White to move:
Without en passant, g2 to g4 looks like checkmate in one. The pawn attacks the king on h5, the bishop guards h6, and White’s king guards g4, g5, and g6. The old rulebook plays it with total confidence. In the real game, Black has exactly one legal reply, and it’s capturing en passant. The attacking pawn disappears and the king is free.
The new rulebook sees that escape coming and finds a different plan. First, bishop to g5. Black’s only legal move is pushing the h-pawn to h3, out of the way. Now g2 to g4 really is checkmate, because there’s no black pawn beside it to capture en passant.
Both plans looked perfect in the agent’s imagination. Only one used the real rules.
What This Taught Me
The thing I keep coming back to is the division of labor.
Neural components are good at the fuzzy parts: perception, intuition, and guessing plausible code. Programs are good at the structured parts: exact prediction, verification, and reuse. Neither alone feels right. An LLM writing world code with nothing checking it is just confident fiction. A rule library with no way to grow is stuck the moment the world surprises it.
There are real limits.
“Verified” here means the rulebook agrees with everything the agent has seen, not that it’s true. A wrong rulebook that happens to fit the data passes every check. The LLM already knew chess, so it recognized en passant rather than discovering it. The environment also hands the agent the list of legal moves, which makes learning much easier than working them out from games alone. Chess is fully symbolic, so I skipped perception, which is where many symbolic systems actually fall apart. And some domains, like how a coworker will react to an email, may never compress neatly into code.
Where I’m Taking It Next
The next step I’m most excited about is letting System 2 teach System 1. When the LLM writes a page that passes every check, add it to the library as a new building block. En passant goes back in, the library grows from 32 rulebooks to 64, and the next time the agent meets it, cheap search finds it instead of expensive generation. Over time the agent builds its own vocabulary for its world.
That’s the DreamCoder idea, and it’s the closest thing I’ve found to turning slow deliberation into fast instinct.
After that:
- Rules the LLM can’t recognize. Chess variants and made-up rules, where it has to infer a law from the counterexample instead of remembering it.
- Probabilistic laws. Rules that only hold some of the time, so “verified” becomes “likely.”
- Perception. Photos of a board instead of symbols, so the neural half of the hybrid has to earn its place.
- Smarter experiments. Prefer the experiment that would throw out the most rulebooks, instead of any disagreement at all.
- Harder worlds. Hidden state like keys and doors, delayed effects, multiple agents. Find where code-as-model stops compressing well.
Takeaway
Code earns its place as a world model because it can be checked. Every observation becomes a test, every failure becomes a precise counterexample, and every plan made in imagination is only as good as the program it runs on.
Try It
The code is on GitHub at contrasam/chessworld. It’s plain Python with no dependencies, and it includes the exact prompts and LLM replies from all five runs.
References
-
TypeSafe AI. Introducing System One Models & Jev. TypeSafe AI Blog, September 2026. https://typesafe.ai/blog/introducing-system-one-models-and-jev
-
Julian Schrittwieser, Ioannis Antonoglou, Thomas Hubert, et al. Mastering Atari, Go, Chess and Shogi by Planning with a Learned Model. Nature 2020. https://arxiv.org/abs/1911.08265
-
Kevin Ellis, Catherine Wong, Maxwell Nye, et al. DreamCoder: Bootstrapping Inductive Program Synthesis with Wake-Sleep Library Learning. PLDI 2021. https://arxiv.org/abs/2006.08381
-
Hao Tang, Darren Key, Kevin Ellis. WorldCoder, a Model-Based LLM Agent: Building World Models by Writing Code and Interacting with the Environment. NeurIPS 2024. https://arxiv.org/abs/2402.12275
-
Nicola Dainese, Matteo Merler, Minttu Alakuijala, Pekka Marttinen. Generating Code World Models with Large Language Models Guided by Monte Carlo Tree Search. NeurIPS 2024. https://arxiv.org/abs/2405.15383
-
Peter W. Battaglia, Jessica B. Hamrick, Joshua B. Tenenbaum. Simulation as an Engine of Physical Scene Understanding. PNAS 2013. https://doi.org/10.1073/pnas.1306572110
-
Daniel Kahneman. Thinking, Fast and Slow. Farrar, Straus and Giroux, 2011.