๐Ÿ My First Python Game ยท Episode 7 of 7 ยท See All Episodes
๐ŸŒฒ Episode 7 ยท Series Finale ยท Upgrades Episode 2

Level Up!
Platformer Part 2

The finale upgrades your Episode 2 platformer into a real game: patrolling enemies you can stomp, a scrolling camera, moving platforms, spikes, checkpoints and a proper multi-level structure.

๐Ÿ‘ถ Ages 10+ โฑ๏ธ ~2 Hours ๐Ÿ Python โœ“ Free
๐Ÿ‘พ Patrol enemies ๐Ÿฅพ Stomp mechanic ๐ŸŽฅ Camera scroll ๐Ÿ›— Moving platforms โš ๏ธ Hazards ๐Ÿšฉ Checkpoints
โญ
0 XP
Level 1
๐Ÿ”ฅ0
Your Progress 0 / 6 steps
๐Ÿงฉ
Need the game so far? This episode continues your platformer from Part 1, so it assumes you already have that pygame window, player, gravity and loop. Open your Part 1 file to keep building, or revisit Part 1 and the base template here: Platformer Part 1 โ†’ ยท Python Cheatsheet โ†’
1
๐Ÿ‘พ
Patrolling Enemies
Walkers that pace between two x positions
Active
๐ŸŽฏ
Goal for this step

Add enemies that patrol platforms and hurt the player on contact.

  • 1Open your Episode 2 platformer, the finale builds on it.
  • 2Enemies: list of dicts, rect, vx, and patrol bounds min_x / max_x.
  • 3Each frame: move by vx; at a bound, flip vx (bounce between posts).
  • 4Touching one costs the player a respawn (for now, stomping comes next). Draw them red.
game.py
enemies = [
    {"rect": pygame.Rect(390, 236, 30, 24), "vx": 2, "min_x": 380, "max_x": 500},
    {"rect": pygame.Rect(60, 416, 30, 24),  "vx": 2, "min_x": 40,  "max_x": 700},
]

for en in enemies:
    en["rect"].x += en["vx"]
    if en["rect"].x <= en["min_x"] or en["rect"].x >= en["max_x"]:
        en["vx"] = -en["vx"]enemies = [
    {"rect": pygame.Rect(390, 236, 30, 24), "vx": 2, "min_x": 380, "max_x": 500},
    {"rect": pygame.Rect(60, 416, 30, 24),  "vx": 2, "min_x": 40,  "max_x": 700},
]

for en in enemies:
    en["rect"].x += en["vx"]
    if en["rect"].x <= en["min_x"] or en["rect"].x >= en["max_x"]:
        en["vx"] = -en["vx"]
๐Ÿ“ฆ Starting point , your finished Episode 2 platformer

This finale builds on Episode 2. If you don't have your Part 1 file handy, here is the complete Episode 2 platformer to start from. Every step below adds enemies, a camera, moving platforms, spikes and levels to it.

game.py
import pygame
pygame.init()
screen = pygame.display.set_mode((800, 480))
pygame.display.set_caption("Sky Hopper")
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 32)
big_font = pygame.font.SysFont(None, 72)

player = pygame.Rect(100, 380, 34, 44)
speed = 5
vy = 0
on_ground = False

platforms = [
    pygame.Rect(0, 440, 800, 40),      # ground
    pygame.Rect(150, 340, 140, 16),
    pygame.Rect(380, 260, 140, 16),
    pygame.Rect(610, 180, 140, 16),
]
coins = [pygame.Rect(205, 300, 18, 18),
         pygame.Rect(435, 220, 18, 18),
         pygame.Rect(665, 140, 18, 18)]
flag = pygame.Rect(660, 130, 16, 50)
score = 0
won = False

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_SPACE and on_ground and not won:
                vy = -16
                on_ground = False

    if not won:
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            player.x -= speed
        if keys[pygame.K_RIGHT]:
            player.x += speed
        if player.left < 0:
            player.left = 0
        if player.right > 800:
            player.right = 800

        vy += 1                 # gravity
        player.y += vy

        on_ground = False
        for plat in platforms:
            if player.colliderect(plat) and vy > 0 and player.bottom - vy <= plat.top:
                player.bottom = plat.top
                vy = 0
                on_ground = True

        for coin in coins[:]:            # loop over a COPY
            if player.colliderect(coin):
                coins.remove(coin)
                score += 1

        if player.colliderect(flag) and len(coins) == 0:
            won = True

        if player.top > 480:             # fell into a pit
            player.x, player.y = 100, 380
            vy = 0

    # draw
    screen.fill((25, 30, 60))
    for plat in platforms:
        pygame.draw.rect(screen, (6, 214, 160), plat)
    for coin in coins:
        pygame.draw.rect(screen, (255, 209, 102), coin)
    pygame.draw.rect(screen, (247, 160, 30), flag)
    pygame.draw.rect(screen, (255, 119, 0), player)
    screen.blit(font.render("Score: " + str(score), True, (255, 255, 255)), (12, 12))
    if won:
        msg = big_font.render("YOU WIN!", True, (255, 209, 102))
        screen.blit(msg, (250, 190))

    pygame.display.flip()
    clock.tick(60)

pygame.quit()import pygame
pygame.init()
screen = pygame.display.set_mode((800, 480))
pygame.display.set_caption("Sky Hopper")
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 32)
big_font = pygame.font.SysFont(None, 72)

player = pygame.Rect(100, 380, 34, 44)
speed = 5
vy = 0
on_ground = False

platforms = [
    pygame.Rect(0, 440, 800, 40),      # ground
    pygame.Rect(150, 340, 140, 16),
    pygame.Rect(380, 260, 140, 16),
    pygame.Rect(610, 180, 140, 16),
]
coins = [pygame.Rect(205, 300, 18, 18),
         pygame.Rect(435, 220, 18, 18),
         pygame.Rect(665, 140, 18, 18)]
flag = pygame.Rect(660, 130, 16, 50)
score = 0
won = False

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_SPACE and on_ground and not won:
                vy = -16
                on_ground = False

    if not won:
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            player.x -= speed
        if keys[pygame.K_RIGHT]:
            player.x += speed
        if player.left < 0:
            player.left = 0
        if player.right > 800:
            player.right = 800

        vy += 1                 # gravity
        player.y += vy

        on_ground = False
        for plat in platforms:
            if player.colliderect(plat) and vy > 0 and player.bottom - vy <= plat.top:
                player.bottom = plat.top
                vy = 0
                on_ground = True

        for coin in coins[:]:            # loop over a COPY
            if player.colliderect(coin):
                coins.remove(coin)
                score += 1

        if player.colliderect(flag) and len(coins) == 0:
            won = True

        if player.top > 480:             # fell into a pit
            player.x, player.y = 100, 380
            vy = 0

    # draw
    screen.fill((25, 30, 60))
    for plat in platforms:
        pygame.draw.rect(screen, (6, 214, 160), plat)
    for coin in coins:
        pygame.draw.rect(screen, (255, 209, 102), coin)
    pygame.draw.rect(screen, (247, 160, 30), flag)
    pygame.draw.rect(screen, (255, 119, 0), player)
    screen.blit(font.render("Score: " + str(score), True, (255, 255, 255)), (12, 12))
    if won:
        msg = big_font.render("YOU WIN!", True, (255, 209, 102))
        screen.blit(msg, (250, 190))

    pygame.display.flip()
    clock.tick(60)

pygame.quit()
โœ๏ธ
Fill in the Blanks
+15 XP
Each enemy stores patrol limits min_x and . Reaching either limit flips , creating the pacing walk.
๐Ÿง 
Knowledge Check
+15 XP
The patrol pattern (move, flip at bounds) is identical to which earlier mechanic?
AThe maze ghost chase
BThe Breakout ball bouncing off the walls
CThe fruit spawn timer
2
๐Ÿฅพ
The Stomp
Land on enemies to squash them; touch them to get hurt
Locked
๐ŸŽฏ
Goal for this step

Implement the most famous mechanic in games: the head-stomp.

  • 1On player-enemy collision, check HOW: falling (vy > 0) with your bottom near their top = STOMP.
  • 2Stomp: remove the enemy, bounce the player (vy = -10), the bounce is what makes it feel amazing.
  • 3Any other contact: hurt, respawn at start (checkpoints later).
  • 4This is exactly the platform-landing check re-aimed at a moving target. Old skill, new use.
game.py
for en in enemies[:]:
    if player.colliderect(en["rect"]):
        if vy > 0 and player.bottom - vy <= en["rect"].top + 8:
            enemies.remove(en)      # SQUASH!
            vy = -10                # bounce off the kill
            score += 5
        else:
            player.x, player.y = 100, 380   # ouch
            vy = 0for en in enemies[:]:
    if player.colliderect(en["rect"]):
        if vy > 0 and player.bottom - vy <= en["rect"].top + 8:
            enemies.remove(en)      # SQUASH!
            vy = -10                # bounce off the kill
            score += 5
        else:
            player.x, player.y = 100, 380   # ouch
            vy = 0
โŒจ๏ธ
Code Challenge
+20 XP
Only squash when falling onto the enemyโ€™s head:
game.py
if vy > if vy >  and player.bottom - vy <= en["rect"].top + 8:
    enemies. and player.bottom - vy <= en["rect"].top + 8:
    enemies.(en)
    vy = (en)
    vy = 
๐Ÿ’ก Hint: Falling means positive vy; the enemy leaves the list; the player rebounds upward with a negative velocity.
๐Ÿง 
Knowledge Check
+15 XP
Why bounce the player after a stomp?
ATo prevent double kills
BFeedback and flow, the bounce confirms the kill and chains into the next jump. It is why Mario feels good
CGravity requires it
3
๐ŸŽฅ
The Scrolling Camera
A wide level with a camera that follows the player
Locked
๐ŸŽฏ
Goal for this step

Grow the level to 2400 px wide and scroll the view to follow the player.

  • 1A camera in 2D is ONE number: cam_x = player.centerx - 400 (keep the player centred).
  • 2Clamp cam_x between 0 and level_width, 800.
  • 3Draw EVERYTHING offset: screen.blit / draw at (thing.x - cam_x, thing.y).
  • 4Movement/collision code does not change at all, the world is real, only the VIEW shifts. Stretch your platforms out to x = 2400 and explore.
game.py
LEVEL_W = 2400

cam_x = player.centerx - 400
cam_x = max(0, min(LEVEL_W - 800, cam_x))

# every draw call subtracts cam_x:
pygame.draw.rect(screen, (255, 119, 0),
    (player.x - cam_x, player.y, player.width, player.height))
for plat in platforms:
    pygame.draw.rect(screen, (6, 214, 160),
        (plat.x - cam_x, plat.y, plat.width, plat.height))LEVEL_W = 2400

cam_x = player.centerx - 400
cam_x = max(0, min(LEVEL_W - 800, cam_x))

# every draw call subtracts cam_x:
pygame.draw.rect(screen, (255, 119, 0),
    (player.x - cam_x, player.y, player.width, player.height))
for plat in platforms:
    pygame.draw.rect(screen, (6, 214, 160),
        (plat.x - cam_x, plat.y, plat.width, plat.height))
โœ๏ธ
Fill in the Blanks
+15 XP
The camera is just one number, , clamped between 0 and LEVEL_W, . Game logic is unchanged, only subtracts the camera.
๐Ÿง 
Knowledge Check
+15 XP
The player never actually stays "centred on screen" in the code. What really happens?
AThe player teleports each frame
BThe world is drawn shifted by cam_x, the player moves through a big world, the window is just a viewport
CThe window moves on your desktop
4
๐Ÿ›—
Moving Platforms
Platforms that carry the player
Locked
๐ŸŽฏ
Goal for this step

Add platforms that patrol, and carry the player with them.

  • 1A moving platform = the enemy patrol pattern applied to a platform: vx + bounds.
  • 2The magic line: when standing ON one, add its vx to the playerโ€™s x, otherwise it slides out from under you!
  • 3Track which platform you landed on this frame; apply its movement after collision.
  • 4Add one crossing a deadly gap. Instant classic level design.
game.py
movers = [{"rect": pygame.Rect(900, 320, 120, 16),
           "vx": 2, "min_x": 900, "max_x": 1200}]

for m in movers:
    m["rect"].x += m["vx"]
    if m["rect"].x <= m["min_x"] or m["rect"].x >= m["max_x"]:
        m["vx"] = -m["vx"]

# in the landing loop, also check movers; if landed on one:
riding = m
# after collisions:
if riding:
    player.x += riding["vx"]     # carried along!movers = [{"rect": pygame.Rect(900, 320, 120, 16),
           "vx": 2, "min_x": 900, "max_x": 1200}]

for m in movers:
    m["rect"].x += m["vx"]
    if m["rect"].x <= m["min_x"] or m["rect"].x >= m["max_x"]:
        m["vx"] = -m["vx"]

# in the landing loop, also check movers; if landed on one:
riding = m
# after collisions:
if riding:
    player.x += riding["vx"]     # carried along!
โŒจ๏ธ
Code Challenge
+20 XP
Carry the player with the platform they stand on:
game.py
if riding:
    player.x += ridingif riding:
    player.x += riding]]
๐Ÿ’ก Hint: Add the platformโ€™s horizontal velocity to the player so they move together.
๐Ÿง 
Knowledge Check
+15 XP
Without the carry line, standing on a moving platform makes the playerโ€ฆ
AMove twice as fast
BSlide off as the platform moves from under them, the platform moves, they do not
CSink through it
5
โš ๏ธ
Spikes & Checkpoints
Deadly tiles and fair respawn points
Locked
๐ŸŽฏ
Goal for this step

Add hazards that kill and checkpoints that make death fair.

  • 1Spikes: a list of Rects sitting on floors/gaps; touching one = death. Draw as red triangles (polygon per spike).
  • 2Checkpoints: flag Rects; touching one sets respawn = (flag.x, flag.y - 44).
  • 3ALL deaths (spikes, enemies, falling) now respawn at respawn, not the level start.
  • 4Design rule: checkpoint BEFORE every hard section, never after. Deaths teach; long walks back just bore.
game.py
spikes = [pygame.Rect(760, 424, 40, 16), pygame.Rect(1400, 424, 80, 16)]
checkpoints = [pygame.Rect(1000, 380, 12, 60), pygame.Rect(1900, 380, 12, 60)]
respawn = (100, 380)

for cp in checkpoints:
    if player.colliderect(cp):
        respawn = (cp.x, cp.y - 44)

def die():
    global vy
    player.x, player.y = respawn
    vy = 0

for sp in spikes:
    if player.colliderect(sp):
        die()spikes = [pygame.Rect(760, 424, 40, 16), pygame.Rect(1400, 424, 80, 16)]
checkpoints = [pygame.Rect(1000, 380, 12, 60), pygame.Rect(1900, 380, 12, 60)]
respawn = (100, 380)

for cp in checkpoints:
    if player.colliderect(cp):
        respawn = (cp.x, cp.y - 44)

def die():
    global vy
    player.x, player.y = respawn
    vy = 0

for sp in spikes:
    if player.colliderect(sp):
        die()
โœ๏ธ
Fill in the Blanks
+15 XP
Touching a checkpoint updates the point. Every kind of death calls one shared () function, fix respawn logic once, it works everywhere.
๐Ÿง 
Knowledge Check
+15 XP
Best checkpoint placement isโ€ฆ
AAt the very end of the level
BRight before each difficult section, retry the challenge, not the walk to it
CRandom
6
๐Ÿฐ
Levels & The Finale
A level system, a final door, and your series wrap
Locked
๐ŸŽฏ
Goal for this step

Chain levels with a load function and finish the series like a boss.

  • 1Wrap each levelโ€™s data (platforms, enemies, movers, spikes, checkpoints, flag) in a function returning a dict, load_level(n) swaps everything.
  • 2The end-of-level door advances n; after the last, roll your victory screen with total score.
  • 3Series wrap, you have built: game loops, input, gravity, lists, grids, cameras, AI, save files, juice. Every 2D game is a remix of these.
  • 4Your real finale: pick an idea you love and build it. You have the toolkit. ๐Ÿ
game.py
def load_level(n):
    if n == 0:
        return {
            "platforms": [pygame.Rect(0, 440, 2400, 40), ...],
            "enemies":   [...],
            "movers":    [...],
            "spikes":    [...],
            "checkpoints": [...],
            "door": pygame.Rect(2320, 380, 30, 60),
        }
    if n == 1:
        return { ... }

level = load_level(0)
current = 0

if player.colliderect(level["door"]):
    current += 1
    if current < 2:
        level = load_level(current)
        die()   # reuse: places player at respawn start
    else:
        state = "CHAMPION"def load_level(n):
    if n == 0:
        return {
            "platforms": [pygame.Rect(0, 440, 2400, 40), ...],
            "enemies":   [...],
            "movers":    [...],
            "spikes":    [...],
            "checkpoints": [...],
            "door": pygame.Rect(2320, 380, 30, 60),
        }
    if n == 1:
        return { ... }

level = load_level(0)
current = 0

if player.colliderect(level["door"]):
    current += 1
    if current < 2:
        level = load_level(current)
        die()   # reuse: places player at respawn start
    else:
        state = "CHAMPION"
โŒจ๏ธ
Code Challenge
+20 XP
Advance through levels using the loader:
game.py
current += 1
if current < current += 1
if current < :
    level = :
    level = (current)
else:
    state = "CHAMPION"(current)
else:
    state = "CHAMPION"
๐Ÿ’ก Hint: Two levels exist; the loader function returns the next one; otherwise you are the champion.
๐Ÿง 
Knowledge Check
+15 XP
Across all 7 episodes, which single idea appeared in EVERY game you built?
AScreen shake
BThe game loop: handle input โ†’ update the world โ†’ draw, repeated 60 times a second
CBrick walls
โœ… See the complete finished Platformer finale , all 6 steps assembled

Patrolling enemies with head-stomps, a 2400px scrolling camera, carry-you moving platforms, deadly spikes, fair checkpoints, and two chained levels ending in a CHAMPION screen , one file. Both levels are completable. Save it as game.pygame.py, run python game.pypython game.py, and play. Stuck on a step? Compare your file against this.

game.py
import pygame
pygame.init()
screen = pygame.display.set_mode((800, 480))
pygame.display.set_caption("Sky Hopper: The Finale")
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 32)
big_font = pygame.font.SysFont(None, 64)
LEVEL_W = 2400

def load_level(n):
    if n == 0:
        return {
            "platforms": [pygame.Rect(0, 440, LEVEL_W, 40),
                          pygame.Rect(300, 340, 140, 16),
                          pygame.Rect(620, 260, 150, 16),
                          pygame.Rect(1500, 340, 160, 16)],
            "enemies": [{"rect": pygame.Rect(500, 416, 30, 24), "vx": 2, "min_x": 460, "max_x": 720},
                        {"rect": pygame.Rect(1700, 416, 30, 24), "vx": 2, "min_x": 1650, "max_x": 1860}],
            "movers": [{"rect": pygame.Rect(900, 320, 120, 16), "vx": 2, "min_x": 900, "max_x": 1240}],
            "spikes": [pygame.Rect(760, 424, 40, 16), pygame.Rect(1400, 424, 80, 16)],
            "checkpoints": [pygame.Rect(1000, 380, 12, 60), pygame.Rect(1900, 380, 12, 60)],
            "coins": [pygame.Rect(350, 300, 18, 18), pygame.Rect(670, 220, 18, 18), pygame.Rect(1560, 300, 18, 18)],
            "door": pygame.Rect(2320, 380, 30, 60),
        }
    return {
        "platforms": [pygame.Rect(0, 440, LEVEL_W, 40),
                      pygame.Rect(450, 330, 140, 16),
                      pygame.Rect(1100, 300, 160, 16),
                      pygame.Rect(1800, 340, 160, 16)],
        "enemies": [{"rect": pygame.Rect(700, 416, 30, 24), "vx": 3, "min_x": 640, "max_x": 980},
                    {"rect": pygame.Rect(1500, 416, 30, 24), "vx": 3, "min_x": 1450, "max_x": 1700}],
        "movers": [{"rect": pygame.Rect(1150, 300, 120, 16), "vx": 3, "min_x": 1100, "max_x": 1500}],
        "spikes": [pygame.Rect(600, 424, 80, 16), pygame.Rect(1300, 424, 80, 16), pygame.Rect(2000, 424, 80, 16)],
        "checkpoints": [pygame.Rect(1050, 380, 12, 60), pygame.Rect(1950, 380, 12, 60)],
        "coins": [pygame.Rect(500, 290, 18, 18), pygame.Rect(1160, 260, 18, 18), pygame.Rect(1850, 300, 18, 18)],
        "door": pygame.Rect(2320, 380, 30, 60),
    }

current = 0
level = load_level(0)
player = pygame.Rect(100, 380, 34, 44)
vy = 0
on_ground = False
respawn = (100, 380)
score = 0
state = "PLAY"

def die():
    global vy
    player.x, player.y = respawn
    vy = 0

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_r and state == "CHAMPION":
                current = 0
                level = load_level(0)
                player.x, player.y = 100, 380
                vy = 0
                respawn = (100, 380)
                score = 0
                state = "PLAY"
            elif event.key == pygame.K_SPACE and on_ground and state == "PLAY":
                vy = -16
                on_ground = False

    if state == "PLAY":
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            player.x -= 5
        if keys[pygame.K_RIGHT]:
            player.x += 5
        if player.left < 0:
            player.left = 0
        if player.right > LEVEL_W:
            player.right = LEVEL_W

        for en in level["enemies"]:
            en["rect"].x += en["vx"]
            if en["rect"].x <= en["min_x"] or en["rect"].x >= en["max_x"]:
                en["vx"] = -en["vx"]
        for m in level["movers"]:
            m["rect"].x += m["vx"]
            if m["rect"].x <= m["min_x"] or m["rect"].x >= m["max_x"]:
                m["vx"] = -m["vx"]

        vy += 1
        player.y += vy
        on_ground = False
        riding = None
        for plat in level["platforms"]:
            if player.colliderect(plat) and vy > 0 and player.bottom - vy <= plat.top:
                player.bottom = plat.top
                vy = 0
                on_ground = True
        for m in level["movers"]:
            if player.colliderect(m["rect"]) and vy > 0 and player.bottom - vy <= m["rect"].top:
                player.bottom = m["rect"].top
                vy = 0
                on_ground = True
                riding = m
        if riding:
            player.x += riding["vx"]        # carried by the platform

        for en in level["enemies"][:]:
            if player.colliderect(en["rect"]):
                if vy > 0 and player.bottom - vy <= en["rect"].top + 8:
                    level["enemies"].remove(en)     # STOMP
                    vy = -10
                    score += 5
                else:
                    die()

        for sp in level["spikes"]:
            if player.colliderect(sp):
                die()

        for cp in level["checkpoints"]:
            if player.colliderect(cp):
                respawn = (cp.x, cp.y - 44)

        for c in level["coins"][:]:
            if player.colliderect(c):
                level["coins"].remove(c)
                score += 1

        if player.top > 480:
            die()

        if player.colliderect(level["door"]):
            current += 1
            if current < 2:
                level = load_level(current)
                player.x, player.y = 100, 380
                vy = 0
                respawn = (100, 380)
            else:
                state = "CHAMPION"

    # ---- draw ----
    cam_x = player.centerx - 400
    cam_x = max(0, min(LEVEL_W - 800, cam_x))
    screen.fill((25, 30, 60))
    for plat in level["platforms"]:
        pygame.draw.rect(screen, (6, 214, 160), (plat.x - cam_x, plat.y, plat.width, plat.height))
    for m in level["movers"]:
        pygame.draw.rect(screen, (129, 140, 248), (m["rect"].x - cam_x, m["rect"].y, m["rect"].width, m["rect"].height))
    for sp in level["spikes"]:
        pygame.draw.polygon(screen, (248, 113, 113),
                            [(sp.x - cam_x, sp.bottom), (sp.centerx - cam_x, sp.y), (sp.right - cam_x, sp.bottom)])
    for cp in level["checkpoints"]:
        pygame.draw.rect(screen, (250, 204, 21), (cp.x - cam_x, cp.y, cp.width, cp.height))
    for c in level["coins"]:
        pygame.draw.ellipse(screen, (255, 209, 102), (c.x - cam_x, c.y, c.width, c.height))
    for en in level["enemies"]:
        pygame.draw.rect(screen, (239, 68, 68), (en["rect"].x - cam_x, en["rect"].y, en["rect"].width, en["rect"].height))
    d = level["door"]
    pygame.draw.rect(screen, (167, 139, 250), (d.x - cam_x, d.y, d.width, d.height))
    pygame.draw.rect(screen, (255, 119, 0), (player.x - cam_x, player.y, player.width, player.height))

    screen.blit(font.render("Score: " + str(score) + "    Level " + str(current + 1), True, (255, 255, 255)), (12, 12))
    if state == "CHAMPION":
        msg = big_font.render("CHAMPION!", True, (255, 209, 102))
        screen.blit(msg, msg.get_rect(center=(400, 210)))
        sub = font.render("Final score " + str(score) + " - press R", True, (255, 255, 255))
        screen.blit(sub, sub.get_rect(center=(400, 260)))

    pygame.display.flip()
    clock.tick(60)

pygame.quit()import pygame
pygame.init()
screen = pygame.display.set_mode((800, 480))
pygame.display.set_caption("Sky Hopper: The Finale")
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 32)
big_font = pygame.font.SysFont(None, 64)
LEVEL_W = 2400

def load_level(n):
    if n == 0:
        return {
            "platforms": [pygame.Rect(0, 440, LEVEL_W, 40),
                          pygame.Rect(300, 340, 140, 16),
                          pygame.Rect(620, 260, 150, 16),
                          pygame.Rect(1500, 340, 160, 16)],
            "enemies": [{"rect": pygame.Rect(500, 416, 30, 24), "vx": 2, "min_x": 460, "max_x": 720},
                        {"rect": pygame.Rect(1700, 416, 30, 24), "vx": 2, "min_x": 1650, "max_x": 1860}],
            "movers": [{"rect": pygame.Rect(900, 320, 120, 16), "vx": 2, "min_x": 900, "max_x": 1240}],
            "spikes": [pygame.Rect(760, 424, 40, 16), pygame.Rect(1400, 424, 80, 16)],
            "checkpoints": [pygame.Rect(1000, 380, 12, 60), pygame.Rect(1900, 380, 12, 60)],
            "coins": [pygame.Rect(350, 300, 18, 18), pygame.Rect(670, 220, 18, 18), pygame.Rect(1560, 300, 18, 18)],
            "door": pygame.Rect(2320, 380, 30, 60),
        }
    return {
        "platforms": [pygame.Rect(0, 440, LEVEL_W, 40),
                      pygame.Rect(450, 330, 140, 16),
                      pygame.Rect(1100, 300, 160, 16),
                      pygame.Rect(1800, 340, 160, 16)],
        "enemies": [{"rect": pygame.Rect(700, 416, 30, 24), "vx": 3, "min_x": 640, "max_x": 980},
                    {"rect": pygame.Rect(1500, 416, 30, 24), "vx": 3, "min_x": 1450, "max_x": 1700}],
        "movers": [{"rect": pygame.Rect(1150, 300, 120, 16), "vx": 3, "min_x": 1100, "max_x": 1500}],
        "spikes": [pygame.Rect(600, 424, 80, 16), pygame.Rect(1300, 424, 80, 16), pygame.Rect(2000, 424, 80, 16)],
        "checkpoints": [pygame.Rect(1050, 380, 12, 60), pygame.Rect(1950, 380, 12, 60)],
        "coins": [pygame.Rect(500, 290, 18, 18), pygame.Rect(1160, 260, 18, 18), pygame.Rect(1850, 300, 18, 18)],
        "door": pygame.Rect(2320, 380, 30, 60),
    }

current = 0
level = load_level(0)
player = pygame.Rect(100, 380, 34, 44)
vy = 0
on_ground = False
respawn = (100, 380)
score = 0
state = "PLAY"

def die():
    global vy
    player.x, player.y = respawn
    vy = 0

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_r and state == "CHAMPION":
                current = 0
                level = load_level(0)
                player.x, player.y = 100, 380
                vy = 0
                respawn = (100, 380)
                score = 0
                state = "PLAY"
            elif event.key == pygame.K_SPACE and on_ground and state == "PLAY":
                vy = -16
                on_ground = False

    if state == "PLAY":
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            player.x -= 5
        if keys[pygame.K_RIGHT]:
            player.x += 5
        if player.left < 0:
            player.left = 0
        if player.right > LEVEL_W:
            player.right = LEVEL_W

        for en in level["enemies"]:
            en["rect"].x += en["vx"]
            if en["rect"].x <= en["min_x"] or en["rect"].x >= en["max_x"]:
                en["vx"] = -en["vx"]
        for m in level["movers"]:
            m["rect"].x += m["vx"]
            if m["rect"].x <= m["min_x"] or m["rect"].x >= m["max_x"]:
                m["vx"] = -m["vx"]

        vy += 1
        player.y += vy
        on_ground = False
        riding = None
        for plat in level["platforms"]:
            if player.colliderect(plat) and vy > 0 and player.bottom - vy <= plat.top:
                player.bottom = plat.top
                vy = 0
                on_ground = True
        for m in level["movers"]:
            if player.colliderect(m["rect"]) and vy > 0 and player.bottom - vy <= m["rect"].top:
                player.bottom = m["rect"].top
                vy = 0
                on_ground = True
                riding = m
        if riding:
            player.x += riding["vx"]        # carried by the platform

        for en in level["enemies"][:]:
            if player.colliderect(en["rect"]):
                if vy > 0 and player.bottom - vy <= en["rect"].top + 8:
                    level["enemies"].remove(en)     # STOMP
                    vy = -10
                    score += 5
                else:
                    die()

        for sp in level["spikes"]:
            if player.colliderect(sp):
                die()

        for cp in level["checkpoints"]:
            if player.colliderect(cp):
                respawn = (cp.x, cp.y - 44)

        for c in level["coins"][:]:
            if player.colliderect(c):
                level["coins"].remove(c)
                score += 1

        if player.top > 480:
            die()

        if player.colliderect(level["door"]):
            current += 1
            if current < 2:
                level = load_level(current)
                player.x, player.y = 100, 380
                vy = 0
                respawn = (100, 380)
            else:
                state = "CHAMPION"

    # ---- draw ----
    cam_x = player.centerx - 400
    cam_x = max(0, min(LEVEL_W - 800, cam_x))
    screen.fill((25, 30, 60))
    for plat in level["platforms"]:
        pygame.draw.rect(screen, (6, 214, 160), (plat.x - cam_x, plat.y, plat.width, plat.height))
    for m in level["movers"]:
        pygame.draw.rect(screen, (129, 140, 248), (m["rect"].x - cam_x, m["rect"].y, m["rect"].width, m["rect"].height))
    for sp in level["spikes"]:
        pygame.draw.polygon(screen, (248, 113, 113),
                            [(sp.x - cam_x, sp.bottom), (sp.centerx - cam_x, sp.y), (sp.right - cam_x, sp.bottom)])
    for cp in level["checkpoints"]:
        pygame.draw.rect(screen, (250, 204, 21), (cp.x - cam_x, cp.y, cp.width, cp.height))
    for c in level["coins"]:
        pygame.draw.ellipse(screen, (255, 209, 102), (c.x - cam_x, c.y, c.width, c.height))
    for en in level["enemies"]:
        pygame.draw.rect(screen, (239, 68, 68), (en["rect"].x - cam_x, en["rect"].y, en["rect"].width, en["rect"].height))
    d = level["door"]
    pygame.draw.rect(screen, (167, 139, 250), (d.x - cam_x, d.y, d.width, d.height))
    pygame.draw.rect(screen, (255, 119, 0), (player.x - cam_x, player.y, player.width, player.height))

    screen.blit(font.render("Score: " + str(score) + "    Level " + str(current + 1), True, (255, 255, 255)), (12, 12))
    if state == "CHAMPION":
        msg = big_font.render("CHAMPION!", True, (255, 209, 102))
        screen.blit(msg, msg.get_rect(center=(400, 210)))
        sub = font.render("Final score " + str(score) + " - press R", True, (255, 255, 255))
        screen.blit(sub, sub.get_rect(center=(400, 260)))

    pygame.display.flip()
    clock.tick(60)

pygame.quit()
๐ŸŽ‰๐Ÿ†๐ŸŽฎโœจ๐ŸŽ‰
Workshop Complete!
Enemies, camera, moving platforms, checkpoints, seven episodes and you are genuinely a Python game developer. Build your own idea next. ๐Ÿ๐Ÿ†
0
Total XP
1
Level
0
Best Streak
0%
Accuracy
โญ 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