Exploring System 1 Decision Models on Pacman
2026/09/27
Most of the AI models I work with generate text. Laya doesn't. Laya is a non-autoregressive "System 1" decision model. Instead of generating a response, you give it a state and a set of typed questions, and it returns probabilities over the possible answers.
I wanted to see what that actually feels like to build with. So I gave it Pac-Man.
The idea was simple. At every step, Pac-Man's current situation gets turned into a small text representation. Laya decides which direction to take, and the game executes the move. Then we repeat.
I wasn't trying to build a competitive Pac-Man agent. Laya wasn't trained to play Pac-Man, and this wasn't meant to be a serious game-playing benchmark. I mostly wanted to see what would happen when you put a decision model inside a closed loop and let it make hundreds of decisions on its own.
What I ended up finding was more interesting than whether it could play the game. Small changes in how I represented the state and described the available moves had a surprisingly large effect on its behavior.
And once I had the whole thing working, I pointed the same setup at another System 1 model, Jev. That second experiment turned out to be the most useful part. It let me separate the things that were actually properties of this kind of decision model from the things that were just quirks of Laya.
The Experiment
I built a small, self-contained version of Pac-Man with a 19×21 maze, dots and power pellets, four ghosts with simplified versions of their usual behaviors, tunnel wrapping, lives, and scoring. The game is also independent of any rendering library, so I could run it completely headless while testing different decision-making setups. Everything in the local Laya experiments was run on an RTX 2060 with 6 GB of VRAM and a Core i5-9400F.
At every step:
The game looks at the current state.
That state is converted into a compact text representation.
The legal moves are turned into option descriptions.
The model chooses a direction.
Pac-Man moves.
The process repeats.
So the loop is essentially:
game state → text representation → model decision → game action → new game state
Figure 1. The basic loop: the game state is converted into text, the decision model chooses an action, and the action advances the game.
I also asked the model a few additional questions about the state, such as whether Pac-Man was in danger and how risky the current situation was. Laya can answer multiple typed questions in the same forward pass, so I could get this extra information without making separate model calls.
The important part is that the model never sees the actual game. It doesn't get the pixels or a structured representation of the maze. Everything it knows about the current situation comes from the text I give it. And that turned out to matter a lot.
Finding 1: The state has a hard budget
My first instinct was to give the model as much information as possible. The initial state contained an ASCII view of the maze, information about the neighboring tiles, the positions and distances of ghosts, Pac-Man's direction, the score, remaining lives, remaining dots, and the legal moves.
It looked roughly like this:
{ "view": ["#####", "#.. #", "#.P G", "# ..#", "#####"], "facing": "down", "neighbors": { "up": {"tile": "wall", "dots_ahead": 0}, "left": {"tile": "dot", "dots_ahead": 3} }, "ghosts": [ {"at": [-2, 3], "dist": 5, "dir": "up", "frightened": false} ], "score": 340, "lives": 2, "dots_left": 120, "power_time_left": 0, "legal_moves": ["left", "right"] }
It seemed reasonable. If the model had more information about the game, it had more information for its decision. There was a problem, though.
Laya's English checkpoint has a maximum sequence length of 512 tokens. After accounting for the instructions and the option text, only roughly 330 to 380 tokens were available for the game state. I measured the state above with the checkpoint's own tokenizer. It was 341 tokens. That was already uncomfortably close to the limit.
Then I looked at how the input was actually assembled:
[CLS] instructions [SEP] [MASK] option_0 [MASK] option_1 ... [SEP] state [SEP]
The state is placed at the end of the sequence, and when the input is too long, it gets truncated from the right. That means the end of the state is the first thing to disappear.
In one crowded situation, the model lost the end of the payload:
..., "power_time_left": 0, "legal_moves": ["up", "down", "left", "right"]
The model could still see the state, but it could no longer see all of the information I expected it to use. This led to a useful serialization rule:
When a model right-truncates its input, the order of your state fields is effectively a priority list.
The information that matters most needs to appear first. The information that can safely disappear should go later. So instead of treating the state as an ordinary dictionary, I started constructing it from most important to least important. That way, the fields the decision actually depends on are physically incapable of being the first things clipped.
But fixing the token limit exposed another problem. I was giving the model a lot of redundant information.
Finding 2: More information made the state worse
Looking at the original state, I realized that I was describing the same situation several times. The ASCII map described the board. neighbors described what was around Pac-Man. ghosts described the threats. Then the option text described the neighboring tiles again.
I wasn't really giving the model more information. I was giving it the same information in several different forms. So I tried the opposite approach. I gave it only the information I thought actually mattered for the next decision.
The new state looked more like this:
{ "legal_moves": ["up", "down", "left", "right"], "situation": "No power pellet is active, so touching a ghost costs a life.", "threats": [ "a dangerous ghost 3 tiles away, 2 up and 1 left, heading down toward Pac-Man" ], "facing": "down", "dots_left": 177 }
The difference was much bigger than I expected. The original representation was 341 tokens. The compact version averaged only 66 tokens, and even in situations with several nearby ghosts it stayed well below the limit. Across 60 sampled states, its worst case was 106 tokens.
Truncation stopped being something I had to think about. The compact state was also easier to reason about. Instead of asking the model to reconstruct what a collection of coordinates and counters meant, I could tell it directly what was relevant to the decision.
For example, instead of:
"power_time_left": 0
I could say:
"No power pellet is active, so touching a ghost costs a life."
The second representation doesn't contain more information. It's just closer to the actual decision being made. That was my first real lesson from the experiment:
A better state is the smallest representation that still contains what the decision depends on.
Finding 3: The answer depends on the order of the options
Once the state was under control, I started looking at the actual decision. I had assumed that if the state stayed the same, changing the order in which the options were presented shouldn't matter. So I tested it. I took one four-way junction, kept the state and option text completely fixed, and asked the same question under all 24 possible orderings of the four options.
The semantic question was identical every time. Only the order changed. The resulting decisions were:
up 2 right 10 down 6 left 6
The model's confidence was extremely low, between 0.001 and 0.04, so the probability distribution was almost flat. That meant the final choice was sometimes being decided by presentation rather than by a meaningful difference between the options. Slot 0 was selected 9 times out of 24, even though each option should have appeared there exactly 6 times.
This was especially interesting because Laya can answer multiple questions in a single forward pass. So instead of running all 24 permutations separately, I could rotate the options:
rotations = [ legal[i:] + legal[:i] for i in range(len(legal)) ]
and ask one question for each rotation in the same call.
Each option appears in every position exactly once. Then I average the probabilities across rotations. This removes the positional effect without requiring 24 separate model calls.
I also started tracking something I called order spread: how much an option's probability changes across the different rotations. A low order spread means the model is making a decision based mostly on the content. A high order spread means presentation is having a large influence on the answer. This ended up being one of the most useful diagnostics in the experiment.
The problem with determinism
Once I had the smaller state representation working, Pac-Man started doing something I hadn't expected. It got stuck. Not because the model crashed. Not because it was confused. It was doing exactly what I had asked it to do.

Pac-Man repeatedly traversing the same cells because the deterministic model keeps making the same decision.
Laya is deterministic. Give it the same state and the same questions, and it gives you the same answer. Most of the time, that's a useful property. It makes the system reproducible and means that running the same experiment twice doesn't introduce randomness into the decision process. But in a game, determinism has an awkward side effect.
Pac-Man would sometimes enter a small corridor and start going back and forth between the same few cells. Once it reached the same state again, Laya made the same decision. That decision led it back into the same state. The cycle repeated forever. The agent had effectively become a finite state machine that had entered a cycle. There was no randomness that could eventually knock it out of the loop.
Interestingly, the original, much larger state had hidden this problem. It contained the exact coordinates of the ghosts, and because the ghosts were moving, the state was slightly different on every visit. That accidental variation was enough to occasionally break the cycle. Once I cleaned up the state, the problem became obvious.
So I needed the environment itself to detect when Pac-Man was stuck. I added a loop detector based on recent movement history. I tested several thresholds:
Trigger | Random walker | Stuck policy |
|---|---|---|
Last 8 moves ≤ 3 cells | 14.0% | 73.5% |
Last 12 moves ≤ 3 cells | 5.7% | 63.2% |
Last 16 moves ≤ 4 cells | 0.0% | 57.3% |
The final rule was:
If the last 16 positions cover 4 or fewer distinct cells, treat it as a loop.
It never fired during ordinary random movement in my test and still caught a substantial fraction of the deliberately stuck behavior. I also had to make the escape behavior sticky. The first version would escape the corridor and then immediately walk back into it one step later. So after breaking a loop, the move that would immediately reverse the escape was temporarily blocked.
This wasn't really about making Laya smarter. The model was doing exactly the same thing it had done before. I was adding a small amount of memory to the environment around it. And that turned out to be an important distinction for the rest of the experiment.
A deterministic decision model doesn't remember that it has already made a bad decision unless that information is represented in its state.
So I stopped treating the loop as a model failure and started treating it as part of the system I was building. The model decides what to do. The environment keeps track of what has already happened.
Finding 4: The wording of an option changes the decision
This was probably the strangest behavior I found. At one junction, one direction had four dots ahead while another was an empty dead end. I expected the model to strongly prefer the direction containing the dots. Instead, with the original option descriptions, it often preferred the dead end.
The options looked roughly like:
down: "next tile empty, 0 dots this way, is a turn" right: "next tile dot, 4 dots this way, is a turn"
The model chose the dot-rich option only about 30% of the time in the original setup. So I started changing the wording while keeping the underlying information the same. I tested several versions:
Option phrasing | P(picks dot-rich option) |
|---|---|
| 0.461 |
| 0.498 |
| 0.614 |
| 0.629 |
The experiment was averaged over six junctions and twelve ordered label pairs so that neither the direction label nor its position could explain the result. 0.50 is chance. The difference was substantial.
The model seemed much more responsive when the option description contained an explicit judgement. Compare:
next tile dot, 4 dots this way
with:
a good move: eats a dot now and more dots lie ahead this way
The underlying information is almost identical. The second one simply tells the model what that information means for the decision.
So I changed the option builder to produce descriptions such as:
"a deadly move: a dangerous ghost is waiting this way" "an excellent move: a power pellet can be eaten this way" "a good move: eats a dot now and more dots lie ahead this way" "a poor move: an empty tile with nothing to eat ahead this way"
For Laya, that made a large difference. But this is where the second model became important.
Is this actually a property of decision models?
At this point, I had learned a lot about how to make Laya behave. But that isn't necessarily the same thing as learning how to work with decision models. Maybe the sensitivity to option wording was a property of System 1 models. Or maybe it was just a strange property of this particular checkpoint. So I wanted a control.
I took the same Pac-Man environment, the same state representation, the same option descriptions, the same loop detector, and the same evaluation code. Then I swapped out Laya for Jev. The important part was that I didn't build a second Pac-Man agent. I put both models behind the same interface:
class Backend: def predict(self, state, questions): ...
Everything above that interface stayed the same. That gave me a much cleaner comparison. If the two models behaved differently, the model itself was the thing that had changed.
What happened in actual Pac-Man?
Once the decision-level experiments were done, I let the models play. I gave each agent five episodes, using the same five seeds and a maximum budget of 600 decisions per episode. I also included a random walker as a baseline.
Episode results
Agent | Avg. score | Best score | Dots | Distinct cells | Loop rescues / 100 | Deaths | Avg. latency |
|---|---|---|---|---|---|---|---|
Jev | 2996 | 3690 | 91.6% | 167.8 | 0.3 | 15 | 370.6 ms |
Laya | 1470 | 2160 | 56.2% | 102.0 | 1.3 | 15 | 74.9 ms |
Random | 170 | 240 | 9.3% | 17.6 | 0.0 | 15 | 0.0 ms |
The score is useful, but it isn't the whole story. distinct cells tells me how much of the maze the agent actually explored. loop rescues / 100 decisions tells me how often the environment had to intervene because the model got stuck. Those metrics make some of the behavioral differences much easier to see.
Jev explored 91.6% of the maze compared with Laya's 56.2%. It also needed the loop detector much less frequently. The random walker never triggered the loop detector, which isn't necessarily a good thing. It simply doesn't tend to spend sixteen consecutive moves circling the same few cells. And every agent eventually lost all three lives.
So this isn't a "who finished Pac-Man?" benchmark. It measures how the different decision policies behave before the ghosts eventually win.
Decision-level results
Episode scores can be noisy, so I also compared the models directly on smaller controlled probes. I used 48 ghost-avoidance scenarios and 12 states sampled from random walks, with byte-identical inputs for both models.
Probe | Laya | Jev |
|---|---|---|
Walks into the ghost | 0.375 | 0.000 |
Criteria polarity, verdict phrasing | 0.614 | 0.718 |
Criteria polarity, terrain phrasing | 0.461 | 0.735 |
Order-flip rate across rotations | 0.333 | 0.083 |
Order-flip rate, walk states only | 0.833 | 0.000 |
Mean order spread | 0.129 | 0.069 |
Mean confidence | 0.123 | 0.724 |
Direction-label prior | down 0.566 / up 0.050 | down 0.540 / up 0.003 |
p50 latency | 71 ms | 361 ms |
The ghost-avoidance result was particularly striking. Jev did not walk into the ghost in any of the 48 scenarios. Laya chose the fatal direction 37.5% of the time, compared with a 46.2% chance baseline.
But the most interesting result for me was the wording experiment. Remember the change that made a big difference for Laya? The verdict-style option descriptions. Jev didn't need it. Jev scored 0.735 with the simpler terrain description, compared with 0.718 with the verdict-style description. So the thing I initially thought might be a general lesson about how System 1 models understand options turned out to be a Laya-specific workaround. That distinction is exactly why I wanted the second model.
The speed tradeoff
The behavioral difference came with a fairly obvious engineering tradeoff. Laya averaged around 71 ms per decision in the decision-level benchmark. Jev averaged around 361 ms. So Jev took a little over five times longer per decision.
The comparison is also not completely apples-to-apples from a deployment perspective. Laya runs locally on the RTX 2060. Jev is accessed over the network. So Laya's latency is a local forward pass while Jev's includes a network round trip. That matters if you're building an actual real-time agent. The point isn't that one number is universally better. It's that the same Pac-Man environment exposed a very different set of tradeoffs between the two models.
The cost of the experiment
The Jev API charged for input tokens. The decision pipeline used roughly 630 input tokens per decision after accounting for the option rotations and auxiliary questions. A full episode cost around $0.013. The five-episode comparison cost roughly $0.064, and the decision-level probe cost around $0.004. The entire comparison in this post came to roughly seven cents. That made it cheap enough to experiment with things like option ordering and alternative prompts rather than treating every model call as something precious.
What actually generalized?
After running the same probes against both models, I ended up with two very different categories of findings. Some appeared in both models. Others were clearly specific to Laya.
Findings that appeared across both models
State representation matters. A huge state isn't automatically a better state. Removing redundant representations reduced the state from 341 tokens to roughly 66 without making the game state less useful.
Serialization matters. When a model right-truncates its input, the order of your fields becomes a priority list.
Option order matters. Both models changed their decisions when the same options were presented in different orders. Rotating the options within a single batched call provides a cheap way to reduce that positional effect.
Option labels can have their own priors. Both models showed a strong preference for certain direction labels even when the option descriptions were identical.
Determinism needs to be designed around. A deterministic model can enter a loop and stay there forever if the state it sees becomes identical. That is something the surrounding agent architecture needs to handle.
Decision-level probes are more informative than long episodes. A Pac-Man score contains the accumulated effects of hundreds of decisions. A carefully designed single-state probe can isolate one behavior and tell you much more about why the model is doing something.
Findings that turned out to be Laya-specific
Verdict-first option descriptions. Laya benefited substantially from descriptions such as:
"a good move: eats a dot now and more dots lie ahead this way"
Jev didn't. In fact, Jev performed slightly better with the simpler terrain description. So I wouldn't generalize this into a rule about all decision models.
Explicitly stating consequences. I also found that saying:
"touching a ghost costs a life"
seemed more useful than:
"power_time_left": 0
What I actually learned
The interesting part was how much of the agent's behavior was determined by the interface between the environment and the model. The model never saw Pac-Man. It saw a description of Pac-Man. And small decisions about that description had large effects:
Which information gets included?
In what order?
How much redundant information is there?
What do the option names look like?
In what order are those options presented?
Does the option description merely describe the world, or does it explain what that information means for the decision?
What happens when the model sees exactly the same state again?
Those are not really Pac-Man questions. They're questions about how to turn an environment into something a decision model can reason about. And the Jev comparison made one thing clear: you have to be careful about turning a model-specific quirk into a general rule.
The biggest improvement I found with Laya, rewriting the option descriptions to include explicit judgements, almost disappeared when I switched models. Meanwhile, the option-order effect and direction-label bias showed up in both. That distinction would have been impossible to see if I had stopped after getting Laya to work.
Getting a decision model to make a choice is easy. Getting it to make useful decisions consistently is much more about the system you build around it.