Dojo Defender: Session 6
Today you add a final boss to Dojo Defender. The boss appears every 5 waves and has 3 phases with their own behaviour. You learn to work with a state machine of its own, HP bars and screen effects.
What you’ll learn today
- State machine for a boss: PHASE_1, PHASE_2, PHASE_3 and EXPLODING
- Drawing an HP bar with a colour gradient (green → yellow → red)
- Homing bullets: a bullet that moves towards the player
- Screen shake: a random offset on the drawing coordinates
- Phase transitions with visual effects
Step 0: Download the starter
The starter is the solution of session 5 — Dojo Defender with parallax stars, particles and splitting asteroids.
- Download the starter for today, unpack it and open
main.py. - Weren’t you there? Download the solution of session 5.
- Click Run. The game works like before. Kill 10 enemies to reach wave 5 — then the boss appears!
Step 1: ✅ Basic — Boss variables
A boss has variables of its own. Add these to the other global variables:
boss = None
boss_active = False
boss_hp = 0
boss_hp_max = 50
boss_phase = "PHASE_1"
boss_timer = 0
boss_phase_transition_timer = 0
boss_defeated_timer = 0
boss_shoot_timer = 0
boss_minion_spawn_timer = 0
boss_bullets = []
boss_minions = []
screen_shake_x = 0
screen_shake_y = 0
boss_direction = 1Also add emptying boss_bullets and boss_minions in reset_game().
Why so many variables? A boss is more complex than ordinary enemies. It has to move, shoot, spawn minions, have phases and a death animation. Every variable is one aspect of that behaviour.
Check
Start the game. Nothing changes yet — but the variables are ready.
Step 2: ⭐ Stretch — Spawning and drawing the boss
Spawn function
Write a function that activates the boss:
def start_boss():
global boss_active, boss, boss_hp, boss_phase, boss_timer
global boss_bullets, boss_minions, boss_shoot_timer
global boss_minion_spawn_timer, boss_direction, screen_shake_x, screen_shake_y
boss_active = True
boss = GameObject('boss.png', WIDTH / 2, -80)
boss_hp = boss_hp_max
boss_phase = "PHASE_1"
boss_timer = 0
boss_shoot_timer = 0
boss_minion_spawn_timer = 0
boss_direction = 1
screen_shake_x = 0
screen_shake_y = 0
boss_bullets.clear()
boss_minions.clear()
roar_path = os.path.join(_DIR, 'sounds/boss_roar.wav')
if os.path.exists(roar_path):
pygame.mixer.Sound(roar_path).play()Boss wave detection
Every 5th wave (from wave 5 on) a boss appears. Add this where the wave goes up:
def check_boss_wave():
if wave >= 5 and wave % 5 == 0 and not boss_active:
start_boss()Call check_boss_wave() after wave += 1.
Stopping enemies during the boss
Make sure ordinary enemies don’t spawn during a boss fight. Put the spawn logic in an if:
if not boss_active:
spawn_counter += 1
if spawn_counter >= spawn_interval:
spawn_counter = 0
spawn_enemy()Drawing the boss
Draw the boss when it’s active, and also draw the HP bar at the top of the screen:
if boss_active and boss is not None:
boss.draw(screen)
# HP bar
bar_width = 300
bar_height = 20
bar_x = (WIDTH - bar_width) // 2
bar_y = 10
bar_bg = pygame.Rect(bar_x, bar_y, bar_width, bar_height)
pygame.draw.rect(screen, (100, 20, 20), bar_bg)
hp_ratio = max(0, boss_hp / boss_hp_max)
fill_width = int(bar_width * hp_ratio)
if hp_ratio > 0.5:
color = (0, 255, 0)
elif hp_ratio > 0.25:
color = (255, 255, 0)
else:
color = (255, 0, 0)
fill_rect = pygame.Rect(bar_x, bar_y, fill_width, bar_height)
pygame.draw.rect(screen, color, fill_rect)
pygame.draw.rect(screen, (255, 255, 255), bar_bg, 2)
label = font.render(f"BOSS {boss_hp}/{boss_hp_max}", True, (255, 255, 255))
screen.blit(label, (bar_x + bar_width // 2 - label.get_width() // 2, bar_y - 5))Check
Play until wave 5. Do you see the boss come in with an HP bar? Ordinary enemies no longer spawn.
Step 3: 🔥 Expert — Boss AI (3 phases)
The boss has a state machine with 3 phases. Every phase is different behaviour.
Phase 1 (HP 34-50, 100-66%)
The boss drifts slowly to the left and right and now and then fires a homing bullet.
def fire_boss_bullet():
dx = ship.rect.centerx - boss.rect.centerx
dy = ship.rect.centery - boss.rect.centery
dist = math.sqrt(dx * dx + dy * dy)
if dist == 0:
dist = 1
speed = 3
bullet = {
'x': boss.rect.centerx,
'y': boss.rect.centery + 30,
'vx': dx / dist * speed,
'vy': dy / dist * speed,
}
boss_bullets.append(bullet)Maths: By dividing dx and dy by the distance (dist) you get a unit vector — a direction with length 1. Multiply by speed for the speed you want. This works in every direction, not just horizontal/vertical.
Add the AI update in the game loop (if boss_active and boss is not None):
if boss.rect.centery < 80:
boss.rect.centery += 2 # coming in
else:
if boss_phase == "PHASE_1":
boss.rect.centerx += boss_direction * 1.5
if boss.rect.centerx > WIDTH - 60:
boss_direction = -1
if boss.rect.centerx < 60:
boss_direction = 1
boss_shoot_timer += 1
if boss_shoot_timer >= 60:
boss_shoot_timer = 0
fire_boss_bullet()Phase transition (HP ≤ 66%)
Check whether the boss has to change phase. Call this after the boss update:
hp_ratio = boss_hp / boss_hp_max
if hp_ratio <= 0.66 and boss_phase == "PHASE_1":
boss_phase = "PHASE_2"
boss_phase_transition_timer = 20During a phase transition you make the boss flash:
if boss_phase_transition_timer > 0 and boss_phase_transition_timer % 4 < 2:
flash_surf = pygame.Surface(boss.image.get_size(), pygame.SRCALPHA)
flash_surf.fill((255, 255, 255, 180))
screen.blit(flash_surf, (boss.rect.x, boss.rect.y))Phase 2 (HP 17-33, 66-33%)
The boss moves faster, fires more often and spawns minions:
elif boss_phase == "PHASE_2":
boss.rect.centerx += boss_direction * 3
...
boss_shoot_timer += 1
if boss_shoot_timer >= 40:
boss_shoot_timer = 0
fire_boss_bullet()
boss_minion_spawn_timer += 1
if boss_minion_spawn_timer >= 180:
boss_minion_spawn_timer = 0
spawn_boss_minion()Spawning a minion:
def spawn_boss_minion():
minion = GameObject('boss_minion.png', random.randint(30, WIDTH - 30), -30)
minion.speed = random.uniform(1, 2)
boss_minions.append(minion)Phase 3 (HP 0-16, 33-0%)
The boss is enraged: fires two bullets at once, spawns minions faster, and the screen shakes:
elif boss_phase == "PHASE_3":
boss.rect.centerx += boss_direction * 4
...
boss_shoot_timer += 1
if boss_shoot_timer >= 20:
boss_shoot_timer = 0
fire_boss_bullet()
if boss_hp < boss_hp_max * 0.15:
fire_boss_bullet()
...Screen shake: add a random offset to ALL the drawing coordinates:
if boss_phase == "PHASE_3":
screen_shake_x = random.randint(-3, 3)
screen_shake_y = random.randint(-3, 3)
else:
screen_shake_x = 0
screen_shake_y = 0Change all the draw functions so they accept offset_x and offset_y parameters. For example draw_stars(screen, screen_shake_x, screen_shake_y).
Check
Play until wave 5. Do you see the boss go through 3 phases? Phase 1: slow, now and then a bullet. Phase 2: faster + minions. Phase 3: screen shakes, rain of bullets.
Step 4: 💀 Expert — Defeating the boss
When the boss reaches 0 HP, the exploding sequence starts:
if boss_hp <= 0:
boss_phase = "EXPLODING"
boss_defeated_timer = 90
boss_phase_transition_timer = 60
score += 500
explode_sound.play()During EXPLODING you spawn a particle burst every 10 frames:
if boss_phase == "EXPLODING":
if boss_phase_transition_timer > 0 and boss_phase_transition_timer % 10 == 0:
spawn_particles(
boss.rect.centerx + random.randint(-40, 40),
boss.rect.centery + random.randint(-40, 40),
random.randint(10, 20),
[(255, 255, 0), (255, 100, 0), (255, 0, 0), (255, 255, 255)],
(2, 6), (3, 7), (20, 40),
)Show “BOSS DEFEATED!” at the top of the screen:
if boss_defeated_timer > 0:
defeat_msg = big_font.render("BOSS DEFEATED!", True, (255, 255, 0))
screen.blit(defeat_msg, (WIDTH // 2 - defeat_msg.get_width() // 2, HEIGHT // 2 - 100))Watch out: after the boss, switch the ordinary enemy spawning back on. The wave continues to the next one.
Check
Defeat the boss. Do you see the explosion animation, the “BOSS DEFEATED!” text and +500 score? Do the ordinary waves continue after that?
Showcase
Show a coach that:
- Boss appears every 5 waves with an HP bar
- 3 phases with their own behaviour (slow → minions → enraged)
- Homing bullets that fly towards the player
- Screen shake in phase 3
- Minions spawning in phase 2 and 3
- Boss defeated: explosion sequence, “BOSS DEFEATED!”, +500 score
See you next time!
“Next time: the polished version. Sound, menu screen, high scores — and your own twist on the game.”
Take it home
Try one of these extensions at home:
- Easy: change the boss colour or size via the image.
- Medium: add a fourth phase (HP < 10%) in which the boss teleports.
- Tricky: let the boss fire delay-bullets that leave a trail.
- Very tricky: add a second boss type that appears in a different wave.