Chapter 14: Scoring

Polish the game. Add a start button, scoring, levels, and high score tracking.

Adding a Play Button

Button Class

# button.py
import pygame.font

class Button:
    """A button for the game."""

    def __init__(self, ai_game, msg):
        self.screen = ai_game.screen
        self.screen_rect = self.screen.get_rect()

        # Button dimensions and properties
        self.width, self.height = 200, 50
        self.button_color = (0, 135, 0)
        self.text_color = (255, 255, 255)
        self.font = pygame.font.SysFont(None, 48)  (1)

        # Build rect and center it
        self.rect = pygame.Rect(0, 0, self.width, self.height)
        self.rect.center = self.screen_rect.center

        self._prep_msg(msg)

    def _prep_msg(self, msg):
        """Render message as image."""
        self.msg_image = self.font.render(
            msg, True, self.text_color, self.button_color  (2)
        )
        self.msg_image_rect = self.msg_image.get_rect()
        self.msg_image_rect.center = self.rect.center

    def draw_button(self):
        """Draw button and message."""
        self.screen.fill(self.button_color, self.rect)
        self.screen.blit(self.msg_image, self.msg_image_rect)
1 None uses default system font
2 True enables antialiasing

Integrating the Button

# In AlienInvasion
from button import Button

def __init__(self):
    # ... existing code ...
    self.game_active = False  (1)
    self.play_button = Button(self, "Play")

def _check_events(self):
    for event in pygame.event.get():
        # ... existing handlers ...
        elif event.type == pygame.MOUSEBUTTONDOWN:
            mouse_pos = pygame.mouse.get_pos()
            self._check_play_button(mouse_pos)

def _check_play_button(self, mouse_pos):
    """Start game when player clicks Play."""
    button_clicked = self.play_button.rect.collidepoint(mouse_pos)
    if button_clicked and not self.game_active:
        self.stats.reset_stats()
        self.game_active = True

        # Clear old game elements
        self.aliens.empty()
        self.bullets.empty()

        # Create new fleet and center ship
        self._create_fleet()
        self.ship.center_ship()

        # Hide mouse cursor
        pygame.mouse.set_visible(False)

def _update_screen(self):
    # ... draw game elements ...

    # Draw button if game inactive
    if not self.game_active:
        self.play_button.draw_button()

    pygame.display.flip()
1 Game starts inactive, waiting for button click

Resetting Speed Settings

Dynamic difficulty - speed increases each level:

# In settings.py
def __init__(self):
    # Static settings
    self.screen_width = 1200
    self.screen_height = 800
    self.bg_color = (30, 30, 30)
    self.ship_limit = 3
    self.bullet_width = 3
    self.bullet_height = 15
    self.bullet_color = (60, 180, 60)
    self.bullets_allowed = 5
    self.fleet_drop_speed = 10

    # Speed increase rate
    self.speedup_scale = 1.1  (1)

    self.initialize_dynamic_settings()

def initialize_dynamic_settings(self):
    """Settings that change during game."""
    self.ship_speed = 1.5
    self.bullet_speed = 2.5
    self.alien_speed = 1.0
    self.fleet_direction = 1

def increase_speed(self):
    """Increase speed settings."""
    self.ship_speed *= self.speedup_scale
    self.bullet_speed *= self.speedup_scale
    self.alien_speed *= self.speedup_scale
1 10% faster each level
# In _check_play_button
self.settings.initialize_dynamic_settings()

Level System

# In game_stats.py
def reset_stats(self):
    self.ships_left = self.settings.ship_limit
    self.level = 1
# In _check_bullet_alien_collisions
if not self.aliens:
    self.bullets.empty()
    self._create_fleet()
    self.settings.increase_speed()
    self.stats.level += 1

Scoring System

Track Score

# In settings.py
self.alien_points = 50

# Later, for dynamic scoring:
self.score_scale = 1.5

def initialize_dynamic_settings(self):
    # ... speed settings ...
    self.alien_points = 50

def increase_speed(self):
    # ... speed increases ...
    self.alien_points = int(self.alien_points * self.score_scale)
# In game_stats.py
def reset_stats(self):
    self.ships_left = self.settings.ship_limit
    self.score = 0
    self.level = 1
# In _check_bullet_alien_collisions
collisions = pygame.sprite.groupcollide(
    self.bullets, self.aliens, True, True
)

if collisions:
    for aliens in collisions.values():  (1)
        self.stats.score += self.settings.alien_points * len(aliens)
1 A bullet can hit multiple aliens - count all

Scoreboard Class

# scoreboard.py
import pygame.font

class Scoreboard:
    """Report scoring information."""

    def __init__(self, ai_game):
        self.screen = ai_game.screen
        self.screen_rect = self.screen.get_rect()
        self.settings = ai_game.settings
        self.stats = ai_game.stats

        self.text_color = (230, 230, 230)
        self.font = pygame.font.SysFont(None, 48)

        self.prep_score()
        self.prep_high_score()
        self.prep_level()
        self.prep_ships()

    def prep_score(self):
        """Render score as image."""
        rounded_score = round(self.stats.score, -1)  (1)
        score_str = f"{rounded_score:,}"  (2)
        self.score_image = self.font.render(
            score_str, True, self.text_color, self.settings.bg_color
        )

        # Position at top right
        self.score_rect = self.score_image.get_rect()
        self.score_rect.right = self.screen_rect.right - 20
        self.score_rect.top = 20

    def prep_high_score(self):
        """Render high score."""
        high_score = round(self.stats.high_score, -1)
        high_score_str = f"{high_score:,}"
        self.high_score_image = self.font.render(
            high_score_str, True, self.text_color, self.settings.bg_color
        )

        self.high_score_rect = self.high_score_image.get_rect()
        self.high_score_rect.centerx = self.screen_rect.centerx
        self.high_score_rect.top = self.score_rect.top

    def prep_level(self):
        """Render level."""
        level_str = str(self.stats.level)
        self.level_image = self.font.render(
            level_str, True, self.text_color, self.settings.bg_color
        )

        self.level_rect = self.level_image.get_rect()
        self.level_rect.right = self.score_rect.right
        self.level_rect.top = self.score_rect.bottom + 10

    def prep_ships(self):
        """Show remaining ships."""
        self.ships = Group()
        for ship_number in range(self.stats.ships_left):
            ship = Ship(self.ai_game)
            ship.rect.x = 10 + ship_number * ship.rect.width
            ship.rect.y = 10
            self.ships.add(ship)

    def show_score(self):
        """Draw scores to screen."""
        self.screen.blit(self.score_image, self.score_rect)
        self.screen.blit(self.high_score_image, self.high_score_rect)
        self.screen.blit(self.level_image, self.level_rect)
        self.ships.draw(self.screen)

    def check_high_score(self):
        """Check for new high score."""
        if self.stats.score > self.stats.high_score:
            self.stats.high_score = self.stats.score
            self.prep_high_score()
1 Round to nearest 10
2 Format with commas: 1,234,500

Using the Scoreboard

# In AlienInvasion
from scoreboard import Scoreboard

def __init__(self):
    # ... existing code ...
    self.sb = Scoreboard(self)

def _check_bullet_alien_collisions(self):
    collisions = pygame.sprite.groupcollide(...)

    if collisions:
        for aliens in collisions.values():
            self.stats.score += self.settings.alien_points * len(aliens)
        self.sb.prep_score()
        self.sb.check_high_score()

    if not self.aliens:
        # ... level up code ...
        self.sb.prep_level()

def _ship_hit(self):
    if self.stats.ships_left > 0:
        self.stats.ships_left -= 1
        self.sb.prep_ships()
        # ... rest of method ...

def _update_screen(self):
    # ... draw game elements ...
    self.sb.show_score()
    # ... rest of method ...

High Score Persistence

# In game_stats.py
from pathlib import Path

class GameStats:
    def __init__(self, ai_game):
        self.settings = ai_game.settings
        self.high_score = self._load_high_score()
        self.reset_stats()

    def _load_high_score(self):
        """Load high score from file."""
        path = Path('high_score.txt')
        if path.exists():
            return int(path.read_text())
        return 0

    def save_high_score(self):
        """Save high score to file."""
        Path('high_score.txt').write_text(str(self.high_score))
# In _ship_hit when game ends
else:
    self.game_active = False
    pygame.mouse.set_visible(True)
    self.stats.save_high_score()

Final Game Loop

def run_game(self):
    """Main game loop."""
    while True:
        self._check_events()

        if self.game_active:
            self.ship.update()
            self._update_bullets()
            self._update_aliens()

        self._update_screen()
        self.clock.tick(60)

def _update_screen(self):
    """Update images and flip to new screen."""
    self.screen.fill(self.settings.bg_color)

    for bullet in self.bullets.sprites():
        bullet.draw_bullet()

    self.ship.blitme()
    self.aliens.draw(self.screen)
    self.sb.show_score()

    if not self.game_active:
        self.play_button.draw_button()

    pygame.display.flip()

Quick Reference

Feature Implementation

Button click

rect.collidepoint(mouse_pos)

Render text

font.render(text, True, color)

Format number

f"{score:,}" (comma separator)

Round score

round(score, -1) (to tens)

Hide cursor

pygame.mouse.set_visible(False)

Dynamic settings

Reset on new game, scale on level up

Exercises

14-1. Press P to Play

Allow starting game with 'P' key in addition to button.

14-2. Target Practice

Create a moving target on right side. Score when hit. Track stats.

14-3. Difficulty Levels

Add Easy/Normal/Hard buttons with different speed settings.

14-4. Historical High Scores

Store top 5 scores in JSON file. Display on game over screen.

Summary

  • Buttons use text rendering and rect collision detection

  • Dynamic settings enable difficulty scaling

  • Scoreboard class renders and positions text images

  • High scores persist using file I/O

  • Game state (active/inactive) controls update logic

  • Text formatting: f-strings with :, for thousands separators

Project 1 complete. Next: Data visualization with matplotlib.