Enemies That Think
Patrols, Chasing & State Machines
A good enemy doesn't need real AI — it needs a handful of well-defined behaviors and clean rules for switching between them. This module builds patrol movement, line-of-sight chasing, a finite state machine, and boss phase patterns.
Patrol Movement
The simplest enemy behavior: walk between two points, reversing direction on arrival. This alone makes a level feel alive before you write a single line of "intelligent" logic.
class Patroller:
def __init__(self, left_x, right_x, y, speed=80):
self.pos = pygame.Vector2(left_x, y)
self.left_x = left_x
self.right_x = right_x
self.speed = speed
self.direction = 1 # 1 = moving right, -1 = moving left
def update(self, dt):
self.pos.x += self.direction * self.speed * dt
if self.pos.x >= self.right_x:
self.direction = -1
elif self.pos.x <= self.left_x:
self.direction = 1
Line-of-Sight Detection & Chasing
Chasing is just "move toward the player's position each frame" — combined with a distance check that decides WHEN to start chasing (usually a detection radius, sometimes with a raycast to confirm nothing blocks the view).
DETECTION_RADIUS = 220
def update_chase(enemy, player_pos, dt):
to_player = player_pos - enemy.pos
distance = to_player.length()
if distance < DETECTION_RADIUS and distance > 0:
direction = to_player.normalize()
enemy.pos += direction * enemy.speed * dt
return True # currently chasing
return False
Finite State Machines
A finite state machine (FSM) keeps enemy logic readable as behaviors multiply. The enemy is always in exactly ONE named state, and transitions between states follow clear rules.
class Enemy:
def __init__(self, pos):
self.pos = pos
self.state = "patrol"
self.speed = 90
def update(self, dt, player_pos):
distance = (player_pos - self.pos).length()
if self.state == "patrol":
self.patrol_update(dt)
if distance < 220:
self.state = "chase"
elif self.state == "chase":
if distance < 40:
self.state = "attack"
elif distance > 300:
self.state = "patrol" # lost the player
else:
direction = (player_pos - self.pos).normalize()
self.pos += direction * self.speed * dt
elif self.state == "attack":
if distance > 60:
self.state = "chase" # player backed away
# else: play attack animation, deal damage
Simple Pathfinding Concepts
Straight-line chasing breaks the moment a wall is in the way. Real pathfinding (like A*) searches a grid for the shortest route around obstacles — a full implementation is beyond this module, but the concept is:
Boss Phases & Attack Timers
Bosses extend the FSM idea with phases tied to remaining health, and attack patterns driven by a cooldown timer rather than player proximity alone.
class Boss:
def __init__(self, max_hp):
self.hp = self.max_hp = max_hp
self.attack_timer = 0
self.phase = 1
def update(self, dt):
if self.hp < self.max_hp * 0.5 and self.phase == 1:
self.phase = 2 # gets more aggressive below 50% HP
self.attack_timer -= dt
if self.attack_timer <= 0:
self.perform_attack()
self.attack_timer = 2.0 if self.phase == 1 else 1.0
def perform_attack(self):
pass # spawn projectiles, slam, etc — varies per phase
Visualizing the Detection Radius
The DETECTION_RADIUS from the chase example is just a circle drawn around the enemy — anyone inside it gets chased, anyone outside it doesn't. The diagram below shows one player who hasn't been noticed yet, and another who has just entered range.