35. Score and text — Homework solutions

Problem 1 — Score display

Problem. A number on screen that the Up/Down arrows change.

How to think about it. One integer variable, two KEYDOWN checks. Re-render the text surface each frame.

Worked solution.

import pygame

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Score Display")
clock  = pygame.time.Clock()
font   = pygame.font.SysFont("arial", 48)

value = 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_UP:
                value += 1
            if event.key == pygame.K_DOWN:
                value = max(0, value - 1)

    screen.fill((20, 20, 40))
    surf = font.render(str(value), True, (255, 255, 255))
    screen.blit(surf, (10, 10))
    pygame.display.flip()
    clock.tick(60)

pygame.quit()

max(0, value - 1) prevents the value from going below 0 without needing a separate if check.

Common mistakes.

  • Using get_pressed for the arrow keys here. get_pressed fires every frame while held, so the number would race upward rapidly. KEYDOWN fires once per press, giving per-press control.

Problem 2 — Countdown timer

Problem. Count down from 10 to 0 and show "Time's up!" at the end.

How to think about it. Add 1/60 to an accumulator each frame. The displayed integer is 10 - int(accumulator), floored at 0. When the accumulator reaches 10 the countdown is done.

Worked solution.

import pygame

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Countdown")
clock  = pygame.time.Clock()
font   = pygame.font.SysFont("arial", 64)
small  = pygame.font.SysFont("arial", 36)

elapsed = 0.0
DURATION = 10.0
done = False

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    if not done:
        elapsed += 1 / 60
        if elapsed >= DURATION:
            elapsed = DURATION
            done = True

    remaining = int(DURATION - elapsed)

    screen.fill((20, 20, 40))

    if done:
        surf = small.render("Time's up!", True, (255, 100, 100))
        screen.blit(surf, ((800 - surf.get_width()) // 2, 260))
    else:
        surf = font.render(str(remaining), True, (255, 255, 255))
        screen.blit(surf, ((800 - surf.get_width()) // 2, 230))

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

pygame.quit()

clock.tick(60) returns the number of milliseconds since the last call — a more accurate alternative for timing is clock.tick(60) / 1000.0 as the delta time. For this exercise, assuming exactly 60 fps with 1/60 is sufficient.

Common mistakes.

  • Computing remaining as DURATION - elapsed as a float and displaying it. The timer shows fractional seconds, which looks unpolished. int(...) truncates to whole seconds.

Problem 3 — Collect and score

Problem. Arrow-key player, repositioning target on touch, score displayed at top.

How to think about it. Combine the collect pattern from chapter 34 with font.render / screen.blit.

Worked solution.

import pygame
import random

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Collect")
clock  = pygame.time.Clock()
font   = pygame.font.SysFont("arial", 28)
W, H   = 800, 600

player = pygame.Rect(370, 270, 50, 50)
target = pygame.Rect(random.randint(0, W-40), random.randint(0, H-40), 40, 40)
score  = 0
speed  = 5

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    keys = pygame.key.get_pressed()
    if keys[pygame.K_LEFT]:
        player.x -= speed
    if keys[pygame.K_RIGHT]:
        player.x += speed
    if keys[pygame.K_UP]:
        player.y -= speed
    if keys[pygame.K_DOWN]:
        player.y += speed

    player.clamp_ip(pygame.Rect(0, 0, W, H))

    if player.colliderect(target):
        score += 1
        target.x = random.randint(0, W - target.width)
        target.y = random.randint(0, H - target.height)

    screen.fill((20, 20, 40))
    pygame.draw.circle(screen, (255, 220, 0), target.center, target.width // 2)
    pygame.draw.rect(screen, (100, 200, 255), player)

    score_surf = font.render(f"Score: {score}", True, (255, 255, 255))
    screen.blit(score_surf, (10, 10))

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

pygame.quit()

Challenge — Three lives

Problem. Collect coins (score), avoid red squares (lives). Display both. Game over at 0 lives.

How to think about it. Keep a list of red squares. Each frame, move each square a fixed amount toward the player (or in a fixed direction). On collision with the player, decrement lives and reposition the square. On collision with a coin, increment score and reposition the coin.

Worked solution.

import pygame
import random

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Three Lives")
clock  = pygame.time.Clock()
font   = pygame.font.SysFont("arial", 28)
large  = pygame.font.SysFont("arial", 64)
W, H   = 800, 600

player    = pygame.Rect(370, 270, 50, 50)
coin      = pygame.Rect(random.randint(0, W-30), random.randint(0, H-30), 30, 30)
enemies   = [pygame.Rect(random.randint(0, W-40), random.randint(0, H-40), 40, 40)
             for _ in range(3)]
score     = 0
lives     = 3
speed     = 4
enemy_speed = 2
game_over = 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_ESCAPE:
                running = False

    if not game_over:
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            player.x -= speed
        if keys[pygame.K_RIGHT]:
            player.x += speed
        if keys[pygame.K_UP]:
            player.y -= speed
        if keys[pygame.K_DOWN]:
            player.y += speed
        player.clamp_ip(pygame.Rect(0, 0, W, H))

        for enemy in enemies:
            if enemy.centerx < player.centerx:
                enemy.x += enemy_speed
            elif enemy.centerx > player.centerx:
                enemy.x -= enemy_speed
            if enemy.centery < player.centery:
                enemy.y += enemy_speed
            elif enemy.centery > player.centery:
                enemy.y -= enemy_speed

            if player.colliderect(enemy):
                lives -= 1
                enemy.x = random.randint(0, W - enemy.width)
                enemy.y = random.randint(0, H - enemy.height)
                if lives <= 0:
                    game_over = True
                    break

        if not game_over and player.colliderect(coin):
            score += 1
            coin.x = random.randint(0, W - coin.width)
            coin.y = random.randint(0, H - coin.height)

    screen.fill((20, 20, 40))
    pygame.draw.circle(screen, (255, 220, 0), coin.center, coin.width // 2)
    for enemy in enemies:
        pygame.draw.rect(screen, (220, 50, 50), enemy)
    pygame.draw.rect(screen, (100, 200, 255), player)

    screen.blit(font.render(f"Score: {score}", True, (255, 255, 255)), (10, 10))
    screen.blit(font.render(f"Lives: {lives}", True, (255, 200, 200)), (10, 44))

    if game_over:
        surf = large.render("Game Over", True, (255, 80, 80))
        screen.blit(surf, ((W - surf.get_width()) // 2, (H - surf.get_height()) // 2))

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

pygame.quit()

The enemies move one step toward the player each frame. When an enemy collides with the player, it respawns at a random position. The break after detecting a lethal hit exits the enemy loop early so only one life is lost per frame even if multiple enemies overlap the player.

Common mistakes.

  • Modifying a list while iterating over it. The solution here only modifies list items' positions, not the list structure itself, so iterating with for enemy in enemies is safe.

Done?

Part 7 ends with two mini-projects — Coin Collector and Collect-All-Coins — that combine the game loop, drawing, movement, and score into complete, playable programs.