Finite state machines for NPC AI
Model NPC behaviour with a finite state machine — distinct states, explicit transition conditions, and cooldowns that prevent rapid state flickering.
- Model an NPC with PATROL, CHASE, FLEE, and ATTACK states
- Define transition conditions using distance, health, and timers
- Explain why transition cooldowns prevent state flickering
The state machine pattern you used in the intermediate tier to manage game screens (title, playing, game-over) applies equally well inside a single NPC. Instead of "what screen is active", the question becomes "what mode of behaviour is this enemy in?"
States and transitions
A combat NPC typically has four states:
PATROL ──────(player in range)──────▶ CHASE
▲ │
│ (player leaves range) │ (health < 30%)
│ ▼
└───────────────────────────────── FLEE
▲
(CHASE, close enough)
│
ATTACKPATROL — the enemy walks a route, unaware of the player. It scans for the player each frame by computing the distance between the two positions.
CHASE — the enemy has seen the player and closes in. If the player gets close enough and the NPC is healthy, it transitions to ATTACK.
ATTACK — the enemy executes its attack animation and deals damage. Most attacks have a cooldown; after the cooldown expires the NPC returns to CHASE if the player is still in range, or to PATROL if not.
FLEE — triggered by low health. The enemy runs away. It might also search for a health pickup or reinforcements, depending on the game design.
Transition conditions
Each arrow in the diagram has a condition. The conditions for a simple melee enemy might be:
| From | To | Condition |
|---|---|---|
| PATROL | CHASE | distance_to_player < DETECTION_RADIUS |
| CHASE | PATROL | distance_to_player > LOSE_SIGHT_RADIUS |
| CHASE | ATTACK | distance_to_player < ATTACK_RADIUS and hp > LOW_HP |
| CHASE | FLEE | hp <= LOW_HP |
| ATTACK | CHASE | attack_cooldown == 0 |
| FLEE | PATROL | distance_to_player > SAFE_RADIUS |
Keep transition conditions simple. Compound conditions (five and clauses) are
hard to debug when the NPC behaves unexpectedly.
Why cooldowns matter
Without a cooldown, an NPC at the boundary between two states flickers: if
the player stands exactly at DETECTION_RADIUS, the enemy transitions PATROL →
CHASE → PATROL → CHASE every frame. This looks broken and can trigger other
systems (like sound effects or animation) dozens of times per second.
The fix is a transition cooldown: after entering a state, prevent leaving it for a minimum number of frames. A value of 30–60 frames (0.5–1 second at 60 fps) is usually enough to smooth the boundary without making the NPC feel unresponsive.
# Sketch of a cooldown guard
MIN_STATE_FRAMES = 45
class Enemy:
def __init__(self):
self.state = "PATROL"
self.state_frames = 0 # frames spent in current state
def transition(self, new_state):
if self.state_frames < MIN_STATE_FRAMES:
return # too soon — ignore this transition
self.state = new_state
self.state_frames = 0
def update(self, player_pos):
self.state_frames += 1
# ... evaluate conditions and call self.transition(...)Hysteresis is a related technique: use a wider radius to lose sight than to gain it. The enemy enters CHASE at 200 px but drops back to PATROL only when the player reaches 280 px. This creates a natural "sticky" zone that prevents flickering without an explicit timer.
Where to go next
Next: NPC state machine — implementing the full PATROL / CHASE / FLEE enemy class in pygame with distance-based transitions.