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 |
|
Render text |
|
Format number |
|
Round score |
|
Hide cursor |
|
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.