Code of the Day
AdvancedGame AI

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.

Game DevAdvanced7 min read
By the end of this lesson you will be able to:
  • 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)

               ATTACK

PATROL — 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:

FromToCondition
PATROLCHASEdistance_to_player < DETECTION_RADIUS
CHASEPATROLdistance_to_player > LOSE_SIGHT_RADIUS
CHASEATTACKdistance_to_player < ATTACK_RADIUS and hp > LOW_HP
CHASEFLEEhp <= LOW_HP
ATTACKCHASEattack_cooldown == 0
FLEEPATROLdistance_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.

Finished reading? Mark it complete to track your progress.

On this page