Most discussions about AI game agents start with the model.
That is probably backwards.
If an AI is supposed to make a decision inside a game, the first question shouldn’t be:
Which model should I use?
It should be:
What exactly does the model get to know?
A game already contains an enormous amount of structured information:
player position
enemy position
health
ammo
weapons
objectives
cover
visibility
nearby enemies
recent damage
team state
map state
The AI does not automatically understand any of it.
The game has to turn that world into a representation the model can actually consume.
That representation is the game state.
And designing that state is arguably one of the most important parts of building a useful AI system.
The architecture I want to test
My previous article looked at TypeSafe AI as a structured decision layer rather than a traditional text-generating LLM.
The architecture looked roughly like this:
Game State
↓
Decision Model
↓
Typed Decision
↓
Game Logic
The next problem is obvious:
What goes inside Game State?
I want to build a small experiment around exactly that question.
The experiment will use a simulated NPC and compare different representations of the same game situation.
Game Simulator
│
▼
Raw State
│
▼
State Builder
│
┌───────┴────────┐
│ │
raw state derived state
│ │
└───────┬────────┘
▼
AI Decision
│
▼
Decision Validator
│
▼
Deterministic Logic
The important part is that the AI doesn’t control the game.
It recommends.
The game decides.
1. Start with the raw game state
Suppose I have a simple combat NPC.
The simulator knows:
{
"npc": {
"health": 23,
"ammo": 2,
"morale": 0.42,
"weapon": "shotgun"
},
"player": {
"health": 72,
"distance": 8,
"recent_damage": 47
},
"environment": {
"cover_available": true,
"cover_quality": 0.8,
"enemy_count": 1,
"ally_count": 0,
"line_of_sight": true
}
}
This is already useful.
But it isn’t necessarily the best representation for a decision model.
The model has to infer things such as:
health is low
ammo is low
the NPC recently took significant damage
the player is close
cover exists
the NPC is outnumbered
That raises an interesting design question:
Should the AI have to infer those things itself?
2. Raw state vs derived state
There are two obvious approaches.
Raw state
{
"health": 23,
"ammo": 2,
"distance": 8,
"recent_damage": 47,
"cover_available": true
}
Derived state
{
"health": 23,
"ammo": 2,
"distance": 8,
"recent_damage": 47,
"cover_available": true,
"health_ratio": 0.23,
"low_ammo": true,
"under_pressure": true,
"escape_possible": true
}
The second representation gives the model more semantic information.
But it also introduces another engineering problem.
Who should calculate those derived values?
My preference is:
The game should calculate deterministic derived features.
For example:
health_ratio = health / max_health
low_ammo = ammo <= 2
under_pressure = (
recent_damage > 30
and distance < 15
)
There is little reason to spend an AI inference call determining something that ordinary code can calculate exactly.
This gives us a useful boundary:
Deterministic computation
↓
Game code
Semantic judgment
↓
AI
That distinction becomes increasingly important as the system grows.
3. Don’t send the entire game
A game might contain thousands of pieces of state.
The AI probably doesn’t need all of them.
For an NPC combat decision, these probably matter:
health
ammo
enemy distance
recent damage
cover
enemy count
ally count
line of sight
escape route
These probably don’t:
NPC cosmetic skin
texture ID
particle effect ID
animation asset path
sound asset ID
unrelated quest metadata
This sounds obvious.
It isn’t always obvious in real systems.
As applications grow, state objects tend to become enormous because every subsystem keeps adding another field.
Eventually someone says:
“Just send the whole object.”
And thus another software architecture is born.
The better approach is to define an explicit AI state projection.
Full Game State
│
▼
AI State Builder
│
▼
Relevant State
The AI should receive a deliberate view of the world rather than the entire world.
4. State is a contract
Once the AI depends on game state, that state becomes an interface.
For example:
class GameState(BaseModel):
tick: int
timestamp_ms: int
npc: NPCState
player: PlayerState
environment: EnvironmentState
That contract should be:
- explicit
- versioned
- validated
- observable
- reproducible
I would therefore include:
{
"schema_version": 1,
"state_version": 184
}
The distinction between those two versions matters.
schema_version means:
What structure does this state follow?
state_version means:
Which point in the simulation does this state represent?
That second value becomes extremely useful once AI calls become asynchronous.
5. The stale decision problem
Imagine the game is running normally.
At tick 184:
NPC health = 30
enemy distance = 12
cover = available
The game sends this state to the AI.
state_version = 184
While the AI is thinking, the game continues.
The player shoots the NPC.
Now the current state is:
state_version = 187
health = 4
Then the AI responds:
state_version = 184
decision = attack
If the game blindly executes that decision, the NPC may attack based on information that is already obsolete.
That’s a serious architectural problem.
The solution is simple:
Every AI decision should reference the state it was generated from.
{
"state_version": 184,
"choice": "attack"
}
Before applying it:
if decision.state_version != current_state.version:
reject(decision)
Now stale decisions become a normal software condition rather than a mysterious AI bug.
6. Don’t ask one giant question
Another experiment I want to test is how much we should ask the model to do in one request.
Instead of:
Given the complete game state,
reason about everything and decide what
the NPC should do.
I would break it down.
For example:
Is the NPC currently under significant threat?
How favorable is the current combat situation?
How useful would retreating be?
What should the NPC do next?
These are much easier to reason about as separate typed decisions.
Conceptually:
Game State
│
├── Threat?
│
├── Combat score?
│
├── Retreat value?
│
└── Action?
The application can then combine those results.
This is also useful for debugging.
If the NPC makes a bad decision, I can inspect:
Threat: true
Combat: very_unfavorable
Retreat value: 5
Action: attack
Now there is an obvious place to investigate.
Without decomposition, you get:
AI said attack.
Excellent. Very scientific. We have learned approximately nothing.
7. The deterministic system still owns the action
Even if the AI returns:
attack
that does not mean the NPC can necessarily attack.
The application still needs to validate it.
if decision.choice == "attack":
if npc.ammo <= 0:
action = "reposition"
elif not can_attack(state):
action = "seek_cover"
else:
action = "attack"
This creates a useful separation:
AI:
"What seems appropriate?"
Game:
"Is that actually allowed?"
The AI handles semantic judgment.
The game handles invariants.
That is a much safer architecture than allowing a model to directly control game state.
8. Confidence changes the architecture
A structured decision becomes more interesting when it also has uncertainty.
Suppose the AI returns:
{
"choice": "seek_cover",
"confidence": 0.84
}
The application can define a policy:
if confidence >= 0.80:
accept()
elif confidence >= 0.60:
accept_with_validation()
else:
use_fallback()
The exact thresholds need to be measured rather than pulled from the sacred book of Arbitrary Numbers.
And confidence shouldn’t be treated as proof that an answer is correct.
It is a signal that needs evaluation and calibration.
9. Latency changes everything
There is another reason I don’t want AI inside the actual frame loop.
At 60 FPS:
1 frame = 16.67 ms
An AI request might take substantially longer.
Therefore:
Frame
↓
AI request
↓
wait
↓
response
↓
next frame
is the wrong architecture.
Instead:
Game Loop
│
├── update
├── render
├── execute current behavior
└── publish AI snapshot
│
▼
AI inference
│
▼
decision queue
│
▼
validator
│
▼
future behavior
The NPC continues operating while the AI is thinking.
That means AI becomes an asynchronous decision source rather than a synchronous part of the frame pipeline.
10. State freshness becomes another variable
Once decisions are asynchronous, state age matters.
Consider:
Decision A
generated from state 100
age = 20 ms
Decision B
generated from state 101
age = 150 ms
Which one should be used?
There isn’t one universal answer.
Different decisions have different freshness requirements.
For example:
combat action:
~100 ms
cover selection:
~300 ms
dialogue choice:
~2 seconds
quest recommendation:
much longer
So I want the experiment to treat freshness as a first-class property.
{
"state_version": 184,
"created_at": 1758512200000,
"expires_at": 1758512200300
}
Now the application can reason about whether an AI decision is still useful.
11. Build a deterministic baseline
This is probably the most important part of the experiment.
I don’t want to build an AI system and then declare victory because the NPC moved around and looked intelligent.
I want a baseline.
Something simple:
def baseline_action(state):
if state.npc.health < 20:
return "retreat"
if state.environment.cover_available:
if state.player.recent_damage > 30:
return "seek_cover"
if state.player.distance < 10:
return "attack"
return "idle"
Then run the exact same game states through:
Same state
│
┌──────────┴──────────┐
▼ ▼
Rule-based AI Semantic AI
│ │
└──────────┬──────────┘
▼
Evaluator
Now there is something measurable.
12. The experiments
I want to test at least five versions of state.
Experiment A: Minimal state
health
ammo
distance
cover
recent damage
Experiment B: Derived state
Add:
health ratio
low ammo
under pressure
escape possible
Experiment C: Temporal state
Add:
recent actions
time since damage
damage received recently
previous decision
Experiment D: Irrelevant state
Intentionally add things such as:
cosmetic ID
skin
unrelated metadata
Then see whether decisions become less stable.
Experiment E: Stale state
Artificially delay responses and measure what happens when decisions arrive late.
This is much more interesting than simply asking:
“Does AI work?”
The better question is:
Which representation of the game state produces useful, stable decisions?
13. What I want to measure
Every decision should generate a record like:
{
"tick": 421,
"state_version": 184,
"state_size_bytes": 812,
"decision": "seek_cover",
"confidence": 0.84,
"latency_ms": 142,
"state_age_ms": 34,
"fallback": false,
"baseline_action": "seek_cover",
"evaluator_score": 5
}
Then I can measure:
Latency
p50
p95
p99
Reliability
timeouts
invalid responses
stale responses
fallback rate
Decision behavior
baseline agreement
evaluator score
decision distribution
State characteristics
serialized state size
number of fields
derived fields
temporal context size
Now the blog isn’t just:
“I think this architecture is interesting.”
It becomes:
“I tested three state representations across N simulated decisions and here’s what changed.”
That is a much more useful engineering article.
14. The architecture I’m aiming for
The final system should look roughly like this:
GAME
│
▼
┌─────────────────┐
│ Game Simulation │
└────────┬────────┘
│
raw state
│
▼
┌─────────────────┐
│ State Builder │
│ │
│ validation │
│ derived values │
│ versioning │
└────────┬────────┘
│
AI state
│
▼
┌─────────────────┐
│ Decision Model │
│ │
│ Choice │
│ Score │
│ Boolean/Noul │
└────────┬────────┘
│
decision + confidence
│
▼
┌─────────────────┐
│ Validator │
│ │
│ freshness │
│ confidence │
│ allowed action │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Behavior System │
│ │
│ rules / BT / │
│ utility logic │
└────────┬────────┘
│
▼
GAME
The important boundary is here:
Semantic uncertainty
│
▼
AI
│
▼
Deterministic execution
The AI doesn’t need to own the whole system.
It needs to own the part that ordinary rules struggle to express.
15. The experiment I actually care about
The interesting question isn’t whether a model can produce:
attack
A model can do that.
The interesting question is whether the representation of the world changes the quality of that decision.
For example:
State A
health = 23
ammo = 2
distance = 8
cover = true
recent_damage = 47
versus:
State B
health = 23
ammo = 2
distance = 8
cover = true
recent_damage = 47
health_ratio = 0.23
low_ammo = true
under_pressure = true
escape_possible = true
versus:
State C
everything above
+ recent actions
+ damage history
+ previous decisions
+ temporal context
Does more information help?
Does derived information help?
At what point does additional context stop helping?
Does irrelevant state create instability?
How quickly does a decision become stale?
How much latency can the architecture tolerate?
Those are engineering questions.
And they are much more interesting than simply asking which AI model is “smarter.”
16. The broader lesson
The more I think about AI inside conventional software, the more the model itself starts looking like only one component.
The real architecture becomes:
World
↓
State representation
↓
Semantic inference
↓
Validation
↓
Deterministic execution
↓
World
The difficult part isn’t only inference.
It is deciding:
What does the model know?
What does it not know?
When was that information true?
Which parts should be calculated deterministically?
How much context is enough?
What happens when the model is wrong?
What happens when the response arrives late?
Who owns the final action?
That makes state design an AI engineering problem in its own right.
Final takeaway
I started this experiment thinking the interesting part would be the AI decision model.
I’m increasingly convinced the more important component is the boundary around it.
A useful game AI system probably shouldn’t look like:
Game → AI → Everything
It should look more like:
Game
↓
Carefully designed state
↓
AI semantic judgment
↓
Typed decision + uncertainty
↓
Validation
↓
Deterministic game logic
The AI gets a structured view of the world.
The application decides what that view means operationally.
And the next experiment is to find out how much the quality of the decision depends on how that world was represented in the first place.
