๐Ÿ My First Python Game ยท Episode 3 of 7 ยท See All Episodes
๐ŸŽฏ Episode 3 ยท Beginner+ ยท Builds on Episodes 1-2

Stay Alive!
Dodge & Collect

Gems rain down, so do bombs. Grab the good, dodge the bad, chase a high score. You will master lists of moving objects, spawn timers, and difficulty curves.

๐Ÿ‘ถ Ages 10+ โฑ๏ธ ~1.5 Hours ๐Ÿ Python โœ“ Free
๐Ÿ“‹ Lists of objects โฒ๏ธ Spawn timers ๐ŸŽฒ Weighted random ๐Ÿ’ฃ Hazards ๐Ÿ“ˆ Difficulty curve ๐Ÿ… High score
โญ
0 XP
Level 1
๐Ÿ”ฅ0
Your Progress 0 / 6 steps
1
๐Ÿƒ
Setup & the Runner
Template + a player locked to the ground line
Active
๐ŸŽฏ
Goal for this step

Standard setup with a player that runs along the bottom of the screen.

  • 1New game.py with the usual template (800ร—480).
  • 2Player Rect at the bottom: y fixed at 430, x moves with arrows at speed 8, this game is about horizontal reflexes.
  • 3Clamp to the screen edges (Episode 1 skill!).
  • 4Draw the player teal.
game.py
player = pygame.Rect(380, 430, 40, 40)

keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
    player.x -= 8
if keys[pygame.K_RIGHT]:
    player.x += 8
player.x = max(0, min(800 - player.width, player.x))
โœ๏ธ
Fill in the Blanks
+15 XP
This game locks the playerโ€™s position and only moves along . The clamp uses max and together.
๐Ÿง 
Knowledge Check
+15 XP
Player speed is 8 here vs 5 in the platformer. What does higher speed change?
ANothing noticeable
BThe game feel, faster reactions possible, so falling objects can be faster too
CThe window size
2
โฒ๏ธ
The Spawn Timer
Spawn a new falling object every 40 frames
Locked
๐ŸŽฏ
Goal for this step

Build a frame-counting timer that spawns falling gems into a list.

  • 1Falling things live in a list. Each is a small dictionary: {"rect": Rect, "kind": "gem"}.
  • 2A spawn timer is just a counter: spawn_timer += 1 each frame; at 40, spawn and reset to 0.
  • 340 frames at 60 FPS โ‰ˆ 0.66 seconds between spawns.
  • 4Move every object down each frame, and delete the ones past the bottom (the [:] copy trick).
game.py
import random
things = []
spawn_timer = 0

# inside the loop:
spawn_timer += 1
if spawn_timer >= 40:
    spawn_timer = 0
    things.append({
        "rect": pygame.Rect(random.randint(10, 766), -24, 24, 24),
        "kind": "gem"
    })

for t in things[:]:
    t["rect"].y += 5
    if t["rect"].y > 480:
        things.remove(t)
โŒจ๏ธ
Code Challenge
+20 XP
Complete the spawn timer:
game.py
spawn_timer += 1
if spawn_timer >= :
    spawn_timer = 
    things.({"rect": pygame.Rect(random.randint(10, 766), -24, 24, 24), "kind": "gem"})
๐Ÿ’ก Hint: Spawn at the threshold, reset the counter, and add to the end of the list.
๐Ÿง 
Knowledge Check
+15 XP
A frame-counter timer at 60 FPS with threshold 40 spawns roughly everyโ€ฆ
A40 seconds
B0.66 seconds (40 รท 60)
C4 seconds
3
๐Ÿ’ฃ
Bombs: Weighted Random
25% of spawns are bombs, colour-coded danger
Locked
๐ŸŽฏ
Goal for this step

Mix hazards into the spawn using weighted randomness.

  • 1At spawn time, roll: kind = "bomb" if random.random() < 0.25 else "gem".
  • 2random.random() gives 0.0-1.0, so < 0.25 is a 25% chance.
  • 3Draw gems as teal diamonds (ellipse) and bombs as red circles, instant readability matters more than pretty.
  • 4Tune the ratio: 40% bombs is panic, 10% is a snooze.
game.py
kind = "bomb" if random.random() < 0.25 else "gem"
things.append({
    "rect": pygame.Rect(random.randint(10, 766), -24, 24, 24),
    "kind": kind
})

# drawing:
for t in things:
    colour = (248, 113, 113) if t["kind"] == "bomb" else (77, 217, 224)
    pygame.draw.ellipse(screen, colour, t["rect"])
โœ๏ธ
Fill in the Blanks
+15 XP
random.random() returns a number between 0.0 and . Checking < 0.25 gives a % chance of a bomb.
๐Ÿง 
Knowledge Check
+15 XP
To make bombs rarer, 10%, the check becomesโ€ฆ
Arandom.random() < 0.10
Brandom.random() > 0.10
Crandom.randint(0, 10)
4
๐Ÿ’ฅ
Catch or Boom
Gems score, bombs cost a life
Locked
๐ŸŽฏ
Goal for this step

Handle both collision outcomes and show score + lives.

  • 1Loop the list (copy!), test colliderect with the player.
  • 2Gem โ†’ score += 1, remove. Bomb โ†’ lives -= 1, remove.
  • 3Missing a GEM is fine in this game, only bombs hurt. Different rules than Episode 1, notice the design choice!
  • 4HUD: score left, hearts right.
game.py
score, lives = 0, 3

for t in things[:]:
    if player.colliderect(t["rect"]):
        if t["kind"] == "gem":
            score += 1
        else:
            lives -= 1
        things.remove(t)
โŒจ๏ธ
Code Challenge
+20 XP
Branch on what the player touched:
game.py
if player.colliderect(t["rect"]):
    if t["kind"] == :
        score += 1
    else:
        lives -= 
    things.(t)
๐Ÿ’ก Hint: Compare the kind string; bombs cost one life; either way the object leaves the list.
๐Ÿง 
Knowledge Check
+15 XP
In Episode 1 missing fruit lost a life; here only touching bombs does. Why vary the rules?
APygame versions differ
BDifferent rules create different tension, dodging vs catching are different skills
CLives are deprecated
5
๐Ÿ“ˆ
The Difficulty Curve
Faster falls, quicker spawns, more bombs over time
Locked
๐ŸŽฏ
Goal for this step

Scale three difficulty knobs with score so the game always ends.

  • 1Fall speed: 5 + score // 10.
  • 2Spawn threshold: max(12, 40 - score // 2), faster spawns but never below 12 frames.
  • 3Bomb chance: min(0.5, 0.25 + score * 0.005), creeps up, capped at 50%.
  • 4A good arcade game is unbeatable by design, the difficulty curve guarantees an exciting death. Chase the high score instead!
game.py
fall_speed = 5 + score // 10
spawn_at = max(12, 40 - score // 2)
bomb_chance = min(0.5, 0.25 + score * 0.005)

if spawn_timer >= spawn_at:
    ...
    kind = "bomb" if random.random() < bomb_chance else "gem"
๐Ÿ’ก
min() and max() as caps and floors, the same clamp trick you use on positions works on difficulty too.
โœ๏ธ
Fill in the Blanks
+15 XP
The spawn threshold has a floor of frames thanks to max(), and the bomb chance is capped at thanks to min().
๐Ÿง 
Knowledge Check
+15 XP
Why cap the bomb chance at 50%?
APython floats stop at 0.5
BPast a point the game stops being hard-but-fair and becomes pure luck, caps protect the fun
CBombs are expensive to draw
6
๐Ÿ…
High Score & Game Over
Persist the best score to a file, real saving!
Locked
๐ŸŽฏ
Goal for this step

End the game cleanly and save the high score to disk so it survives restarts.

  • 1Game over at 0 lives, freeze updates, show final score and press-R (Episode 1 pattern).
  • 2Load the high score at startup from highscore.txt (default 0 if the file does not exist, use try/except).
  • 3On game over, if score beats it: write the new record back to the file.
  • 4That is genuine file I/O, your first persistent save data!
game.py
def load_highscore():
    try:
        with open("highscore.txt") as f:
            return int(f.read())
    except:
        return 0

def save_highscore(value):
    with open("highscore.txt", "w") as f:
        f.write(str(value))

high = load_highscore()

# when the game ends:
if score > high:
    high = score
    save_highscore(high)
โŒจ๏ธ
Code Challenge
+20 XP
Save a new record only when it IS a record:
game.py
if score > :
    high = score
    (high)
๐Ÿ’ก Hint: Compare against the loaded best, then call the writing function.
๐Ÿง 
Knowledge Check
+15 XP
Why wrap the file-reading in try/except?
AFiles are dangerous
BThe first ever run has no highscore.txt, except returns a default 0 instead of crashing
CTo make it faster
๐ŸŽ‰๐Ÿ†๐ŸŽฎโœจ๐ŸŽ‰
Workshop Complete!
Spawn timers, object lists and a difficulty curve, you built a proper score-chaser. Episode 4 slows things down with a maze to solve.
0
Total XP
1
Level
0
Best Streak
0%
Accuracy
โ–ถ Episode 4: Maze Explorer โ†’
โญ View My Progress & Certificates

This workshop was free and took many hours to build. If it helped you learn something new, consider supporting the project.

☕ Support on Ko-fi