2026-09 · experiments

Frogger Played by Jev, Zero Shot State Machine Solver in 180ms

An AI plays Frogger one API call at a time. Two wrong assumptions, both fixed by the agent that made them, and a second half to the scripts argument.

My grandpa had an Atari 2600, and when the family got together my siblings, my cousins and I would crowd around it and fight over whose turn it was. Frogger was one of the cartridges. I can still hear the sound it made when you got flattened, mostly because I heard it a lot. So when I went looking for something to point a new kind of AI model at, I didn't look very hard.

The first time that model played my Frogger clone it made 114 decisions in 45 seconds and chose "wait" zero times.

It hopped. Up, left, right, up, right. It died four times and every death was a hop to the right. Twice that hop went into a cell where the state it had just been sent said, in plain JSON, "ticksUntilHitRight": 0.

Every one of those calls also carried a second question: is the frog about to be hit if it stays still? Sitting on a log, 99 ticks of safety in every direction that mattered, it answered 0.83.

Fast, confident, correctly formatted, and wrong. I've seen that combination before.

After the fixes. Every hop is one API call. The pink bar is the move it took, the panel on the right is what it was shown. Play it live.

What's driving the frog

TypeSafe's Jev is what they call a System One model, and it generates no text at all. You send it a JSON state and a set of typed questions. It sends back typed answers with probabilities attached. There are three kinds of question. A Choice picks one option from a list you supply and returns a probability for every option. A Score grades the state against an ordered rubric. A Noul (their word) asks whether a statement is true and returns a single number between 0 and 1.

The frog asks three questions per call, answered together: a Choice over up, left, right, down and wait, plus two Nouls, "am I in danger" and "am I one hop from a lily pad." Round trip from my browser through a 59-line Node proxy averages 183 ms, 228 ms at p90. Fast enough to drive a game. Slow enough that the latency becomes part of the game, which I'll get to.

If you were around for the OpenAI Gym days this will look familiar. The game is the environment. A JSON snapshot of the board is the observation. The five moves are a discrete action space. Jev sits in the slot where you would have put a reinforcement learning agent, the kind that learned Atari games from the screen. Two things are different, and they matter. Jev is zero-shot: there is no training run, no reward function, no ten million episodes of dying, and it never gets better at Frogger. It also never sees the screen. It gets JSON, about four times a second, and by the time that JSON arrives the game's code has already done the physics. With no weights to tune, the only thing left to tune was the observation, and that turned out to be the whole story.

A friend asked whether Jev was running underneath Claude. It wasn't. Jev had no part in building any of this. Claude Code, running Fable 5.1, wrote the canvas game, the state machine and the proxy. Jev only shows up once the game is running. My part was one spec. The two failures below are assumptions the agent made while building, and it caught and fixed both on its own by watching the frog die and reading the logs. I mostly watched it watch.

I wanted to try it because of where the last post ended up. Once the deterministic work moves into scripts, what's left for the model is routing: look at the situation, pick a branch. I had been doing that with language models, which is a bit like hiring a novelist to work a turnstile. Here was a model built for the turnstile. Frogger seemed like a cheap way to find out what it's like to work with, since a frog gets hit by a truck in a way that a misrouted support ticket does not.

Failure one: numbers it could read but not feel

The first version of the state was an engineer's state. For each direction, how many ticks until that cell kills you. 99 means safe for a long while. 0 means there is a truck in it now.

Look at ticksUntilHitRight: 0 for half a second and tell me if zero is good. Zero hits? Zero ticks left? It's obvious to whoever wrote the field and to nobody else. Jev gets one glance, about 180 milliseconds of it, and nowhere to work anything out. A gut-check model had been handed a field that needed a moment's thought.

Claude Code got there by logging the exact state in front of every death and lining the four of them up. Its fix was one more field.

"hops": { "up": "deadly", "left": "safe", "right": "deadly",
          "down": "safe", "wait": "risky" }

Same information. Code still computes every number, and the thresholds are plain constants: under 15 ticks is deadly, under 30 is risky. The only thing that changed is what gets handed over. Before, measurements. Now, the word you would say out loud if someone asked you about that cell.

raw ticks with verdicts
chose "wait" 0 of 114 102 of 200
danger answer while actually safe about 0.8 0.27
lily pads filled 2 in 45 s, 4 deaths 4 in 60 s, 1 death

Last time the lesson was to keep arithmetic away from the model. This is the second half of that lesson, and I only saw it because the agent tripped over it in front of me. Computing the number in code is only most of the job. Somebody still has to decide what the number means, and that somebody should also be code.

Failure two: predicting the wrong future

I like the second one better.

A hop doesn't happen when Jev is asked. It happens when the answer arrives. So "is the cell above safe" really means "will it be safe about 180 ms from now," and the agent's first version used the rolling average latency to look that far ahead. Sensible. I would have done the same.

A fresh game then opened like this. First call, 363 ms, because the connection was cold. The lookahead learned that answers take 363 ms. Second call: the cell above will be clear by then, says the code. Safe, says Jev, go up. That answer came back in 130 ms. The car had not finished leaving.

The code had asked what the world looks like at the average arrival time. Claude Code pulled the first dozen decisions of that game, saw the 363 next to the 130, and rewrote the question. The answer can land anywhere in a window, so the useful question is what's the worst thing that could be in that cell at any point between the fastest plausible reply and the slowest recent one. A cell is safe only if it's safe whenever the answer turns up. It checked the new math with a scripted bot that picks the obvious move and then acts on a random 80 to 300 ms delay. Three lily pads in 50 seconds, no deaths, and Jev hadn't touched it yet.

If you are putting any model in a loop with something that moves, I think this is the part that transfers. Latency goes in the physics. And it goes in as a distribution, because the day you plan around the mean is the day a reply shows up early.

The one rule where I overrule it

My spec had a single place where code overrides the model. If Jev says wait but also puts danger above 0.7, take its second choice. Everything else, Jev drives, because otherwise what's the demo.

In the last test run the frog was standing on a turtle that had started to dive. Every direction was deadly. Jev said wait, at 92%. It also said danger, 0.84, and it was right about that too. My rule fired and hopped the frog into the river.

It was probably dead either way. I still think about it. Two correct answers went in, and the rule I had asked for, sitting between them, made the decision. So that one is mine.

Three prompts

That's what the build took from me. One spec, one "publish it and make me a GIF," one question about what it all cost. Claude Code did the rest in roughly 60 tool calls. It read the live API docs before writing the client (I had told it not to guess field names, and it didn't). It drew the sprites as ASCII art and compiled them to SVG. It took screenshots of its own work and critiqued them between rounds, which is how I learned the first frog looked like a beetle and the first race car looked like an eyeball. It found and fixed both failures above without being asked. Then it deployed the thing to my VM next to the other tenant.

The game is one HTML file with no build step, and the server has no dependencies. Before it went public Claude also noticed that the proxy would forward any question a browser sent it to a paid API key, moved the questions server-side, and added a daily cap. I hadn't asked. Development burned about 1,150 Jev calls.

So I keep arriving at the same division of labor from different sides. A language model asked to compute the Sahm rule handed me a beautifully formatted 0.03 where the truth was 1.70. A decision model asked to read tick counts hopped a frog sideways into traffic, once at 95%. In both cases the model had been handed the wrong half of the job. Code is good at "how many ticks until the truck gets here." It has nothing to say about "so, go?" About four times a second that is the only question Jev answers, and you can watch it change its mind.

P.S. Strawberry

My friend Kenny Shultz sent me this. He had asked Jev the one question every new model gets asked.

Jev answering a Choice question, How many R's in strawberry? It picks 2 at 81 percent. The option 3 gets 8 percent. Confidence 76 percent.
Two, at 81%. The right answer polled 8%. Courtesy of Kenny.

I would love to dunk on it, except this is the whole post in one screenshot. Counting letters is arithmetic. Jev took its one glance and went with its gut, and its gut, like mine before coffee, says two. "strawberry".count("r") says 3, every time, in about a microsecond. So count in code, hand over the 3, and then ask Jev something it's actually good at, like whether that's a lot of R's for a fruit.


Play it at frogger.davidder.com. Press H to take the controls away from Jev and see how you do; it keeps showing what it would have done. D dumps the last 200 decisions to the console, state and probabilities included.

← back to writing