In this lesson, we will make a complete mini-game. This means we will keep track of your lives and score. The game will end when you run out of lives, and a 'GAME OVER' message will appear.
In this lesson, we will make a complete mini-game. This means we will keep track of your lives and score. The game will end when you run out of lives, and a 'GAME OVER' message will appear.
Before you start coding, let's check how the tests look now.
Run this command in your terminal:
pytest module-1.3/blueprint-5/quest-60
You will see that some tests are not passing yet. This is normal because you haven't written the code for them.
Here are the tests that are not passing:
test_gamestate_initialization
test_increase_score
test_lose_life
test_game_over_trigger
test_game_over_display
test_active_game_lose_life
test_active_game_brick_hit
Now, let's write the code for game_state.py
.
This file will help keep track of your score, lives, and if the game is over.
Here are some general examples of how you might manage game state variables:
class GameInfo:
def __init__(self):
self.points = 0
self.health = 5
self.is_finished = False
def add_points(self, amount):
self.points += amount
def take_damage(self):
self.health -= 1
if self.health <= 0:
self.is_finished = True
# Example usage:
# game_status = GameInfo()
# game_status.add_points(50)
# game_status.take_damage()
# if game_status.is_finished:
# print("Game is done!")
Next, we'll write the code for main.py
.
In main.py
, you will put all the game parts together. This includes moving the paddle, making the ball bounce, checking for bricks, and showing the score and lives.
Here are some general examples of how you might handle these parts in a game loop:
import pygame
# Example: Handling keyboard input
# for event in pygame.event.get():
# if event.type == pygame.KEYDOWN:
# if event.key == pygame.K_LEFT:
# # Move paddle left
# pass
# elif event.key == pygame.K_RIGHT:
# # Move paddle right
# pass
# Example: Checking for collision and reacting
# if ball.rect.colliderect(paddle.rect):
# ball.speed_y = -ball.speed_y # Reverse ball direction
# Example: Looping through a list of objects (like bricks)
# bricks = [...] # List of brick objects
# for brick in bricks:
# if ball.rect.colliderect(brick.rect):
# # Handle brick hit (e.g., remove brick, increase score)
# pass
# Example: Losing a life and resetting ball
# if ball.rect.bottom > screen_height:
# lives -= 1
# if lives > 0:
# # Reset ball position
# pass
# else:
# # Game over
# pass
# Example: Displaying text
# font = pygame.font.SysFont('Arial', 30)
# score_text = font.render(f"Score: {game_state.score}", True, (255, 255, 255))
# screen.blit(score_text, (10, 10))
# Example: Game over display
# if game_state.game_over:
# game_over_font = pygame.font.SysFont('Arial', 70)
# game_over_text = game_over_font.render('GAME OVER', True, (255, 0, 0))
# game_over_rect = game_over_text.get_rect(center=(screen_width/2, screen_height/2))
# screen.blit(game_over_text, game_over_rect)
Now that you've written your code, let's run the tests one more time.
Run this command in your terminal:
pytest module-1.3/blueprint-5/quest-60
You should see all tests passing:
test_gamestate_initialization
test_increase_score
test_lose_life
test_game_over_trigger
test_game_over_display
test_active_game_lose_life
test_active_game_brick_hit
All tests are passing. You have successfully made a complete mini-game!
Pygame allows you to render text onto the screen using fonts. This is essential for displaying information like scores, lives, or game messages.
The process involves:
pygame.init()
, but pygame.font.init()
is specific).Font
object.Surface
.Rect
for the text surface to position it.import pygame
# Example: Font creation
# Use None for the default font, specify size
font = pygame.font.Font(None, 36)
# Example: Rendering text
# Arguments: text string, anti-aliasing (True/False), color
text_surface = font.render("Hello, Pygame!", True, (255, 255, 255)) # White text
# Example: Positioning text
text_rect = text_surface.get_rect()
text_rect.topleft = (10, 10) # Position at (10, 10)
# Example: Drawing text (inside the game loop)
# screen.fill((0, 0, 0)) # Fill background
# screen.blit(text_surface, text_rect)
# pygame.display.flip()
[Image/Gif: Show a simple Pygame window with static text rendered in the corner.]
Game state refers to the current condition of the game, such as the player's score, remaining lives, or the game's overall status (playing, paused, game over). These are typically stored in variables.
To update the display of these variables, you need to:
# Example: Initial state
score = 0
lives = 3
# Example: Updating state based on an event (inside game logic)
# if collision_with_brick:
# score += 10
# if ball_went_off_bottom:
# lives -= 1
# Example: Re-rendering text after state change (inside game logic or drawing)
# Assuming 'font' and 'screen' are defined
score_text_surface = font.render(f"Score: {score}", True, (255, 255, 255))
lives_text_surface = font.render(f"Lives: {lives}", True, (255, 255, 255))
# Example: Drawing updated text (inside drawing phase)
# screen.blit(score_text_surface, (10, 10))
# screen.blit(lives_text_surface, (screen_width - lives_text_surface.get_width() - 10, 10))
[Image/Gif: Show a score counter incrementing or a lives counter decrementing.]
Game logic often depends on specific conditions being met. For example, a score should only increase when a collision occurs, or a life should only be lost when the ball goes off-screen. if
statements are used to check these conditions and execute code accordingly.
Common conditions involve checking:
sprite.rect.colliderect(other_sprite.rect)
).ball.rect.bottom >= screen_height
).if lives <= 0
).# Example: Checking for ball going off bottom (inside game logic)
# Assuming 'ball' object and 'screen_height' constant exist
if ball.rect.bottom > screen_height:
# This condition is true when the ball is below the screen
print("Ball missed!")
# Trigger actions like losing a life and resetting the ball
# lives -= 1
# ball.reset_position()
# Example: Checking for collision (inside game logic)
# Assuming 'ball' and 'paddle' objects exist
if ball.rect.colliderect(paddle.rect):
print("Ball hit paddle!")
# Trigger actions like bouncing the ball
# ball.speed_y *= -1
[Image/Gif: Show a ball hitting the bottom edge and disappearing, or hitting a paddle and bouncing.]
For games with distinct phases (like a title screen, playing, game over), a simple state machine can manage which logic and drawing code runs. A variable (e.g., game_state
) holds the current state, and if/elif/else
statements control the flow.
# Example: Initializing state
game_state = 'playing' # Possible states: 'playing', 'game_over'
# Example: State transition (inside game logic)
# if lives <= 0:
# game_state = 'game_over'
# Example: Logic and Drawing based on state (inside main loop)
# if game_state == 'playing':
# # Update ball, paddle, check collisions, etc.
# ball.move()
# paddle.move(keys)
# # ... collision checks ...
#
# # Draw playing elements
# screen.fill(BLACK)
# ball.draw(screen)
# paddle.draw(screen)
# # ... draw bricks, score, lives ...
#
# elif game_state == 'game_over':
# # Stop movement (implicitly done by not calling move() above)
#
# # Draw game over screen
# screen.fill(BLACK)
# # ... draw 'GAME OVER' text, final score ...
[Image/Gif: Show a transition from the active game screen to a static "GAME OVER" screen.]
A complete game loop combines all the elements: event handling, updating game state variables, moving objects, checking collisions, managing game state transitions, and drawing everything to the screen. The main loop iterates continuously, performing these steps in order for each frame.
The structure typically looks like:
# Initialize Pygame, screen, clock, game state, objects
# Main game loop
running = True
while running:
# 1. Event Handling (check for quit, key presses)
for event in pygame.event.get():
# ... handle events ...
# 2. Game Logic (based on current state)
# if game_state == 'playing':
# Update object positions (ball.move(), paddle.move())
# Check collisions (ball-paddle, ball-brick)
# Update score/lives based on collisions/misses
# Check for win/lose conditions and update game_state
# elif game_state == 'game_over':
# Handle game over screen logic (e.g., wait for restart input)
# 3. Drawing
screen.fill(background_color)
# if game_state == 'playing':
# Draw all active game objects (paddle, ball, bricks)
# Draw score and lives text
# elif game_state == 'game_over':
# Draw game over message and final score
# 4. Update Display
pygame.display.flip()
# 5. Control Frame Rate
clock.tick(FPS)
# Quit Pygame
[Image/Gif: Show a full, simple Breakout game loop running.]
pygame.mixer
Sound effects enhance the game experience by providing audio feedback for events like collisions.
Steps to add sound:
pygame.mixer.init()
). This is separate from pygame.init()
..wav
, .ogg
, etc.) into a Sound
object (pygame.mixer.Sound("path/to/sound.wav")
). It's good practice to handle potential errors if the file is missing.sound_object.play()
).import pygame
# Example: Initialize mixer (after pygame.init())
# pygame.init()
pygame.mixer.init()
# Example: Load sound file
try:
bounce_sound = pygame.mixer.Sound("assets/bounce.wav")
except pygame.error as e:
print(f"Could not load sound file: {e}")
# Create a dummy object if loading fails to prevent crashes
class DummySound:
def play(self): pass
bounce_sound = DummySound()
# Example: Play sound on event (inside game logic)
# if ball.rect.colliderect(paddle.rect):
# ball.speed_y *= -1
# bounce_sound.play() # Play the sound here
[Image/Gif: Show a visual representation of a sound wave playing when a collision happens.]
Refactoring is the process of restructuring existing computer code without changing its external behavior. The goal is to improve nonfunctional attributes of the software, such as readability, maintainability, and simplicity.
Common refactoring techniques include:
# Example: Before Refactoring (simplified)
# def calculate_total(prices, tax, discount_percent):
# subtotal = 0
# for p in prices:
# subtotal += p
# discount_amount = subtotal * (discount_percent / 100)
# discounted_total = subtotal - discount_amount
# tax_amount = discounted_total * tax
# final_price = discounted_total + tax_amount
# return final_price
# Example: After Refactoring (using extracted functions and better names)
# DEFAULT_TAX_RATE = 0.08
# DEFAULT_DISCOUNT_PERCENTAGE = 10
# def calculate_subtotal(item_prices):
# """Calculates the sum of all item prices."""
# return sum(item_prices)
# def apply_discount(amount, discount_percentage):
# """Applies a discount to a given amount."""
# discount_amount = amount * (discount_percentage / 100.0)
# return amount - discount_amount
# def apply_tax(amount, tax_rate):
# """Applies tax to a given amount."""
# tax_amount = amount * tax_rate
# return amount + tax_amount
# def process_order(item_prices, tax_rate=DEFAULT_TAX_RATE, discount_percentage=DEFAULT_DISCOUNT_PERCENTAGE):
# """Processes a customer's order."""
# subtotal = calculate_subtotal(item_prices)
# discounted_total = apply_discount(subtotal, discount_percentage)
# final_price = apply_tax(discounted_total, tax_rate)
# return final_price
Refactoring makes code easier to understand, test, and modify in the future. It's an ongoing process, not a one-time task.