In this lesson, we will make our game know when you win or lose.
If the ball goes off the screen, you lose. If you break all the bricks, you win! We will make messages appear to tell you what happened.
In this lesson, we will make our game know when you win or lose.
If the ball goes off the screen, you lose. If you break all the bricks, you win! We will make messages appear to tell you what happened.
Before you start coding, let's check how the tests look now.
Run this command in your terminal:
pytest module-1.3/blueprint-6/quest-30
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_loss_condition_stops_game_and_shows_message
test_win_condition_stops_game_and_shows_message
test_game_continues_when_active
Now, let's write the code in main.py
to make our game know when you win or lose.
Think about how you can check if the ball has gone off the bottom of the screen. If it has, the game should stop, and a 'Game Over' message should appear.
Also, think about how you can check if all the bricks are gone. If they are, the game should stop, and a 'You Win!' message should appear.
Here are some general ideas for checking conditions and setting messages:
# Example: Checking if a value is too low
player_health = 5
if player_health <= 0:
print("Game Over!")
game_is_running = False
# Example: Checking if a list is empty
items_left = ['item1', 'item2']
if not items_left: # This means the list is empty
print("All items collected! You win!")
game_is_running = False
# Example: Setting a message
message = ""
if game_is_running:
message = "Keep playing!"
else:
message = "Game has ended."
print(message)
Use these ideas to help you add the win and lose conditions to your game in main.py
.
Now that you've written your code, let's check if it works!
Run this command in your terminal:
pytest module-1.3/blueprint-6/quest-30
If your code is correct, all the tests should now pass.
Here are the tests that should pass:
test_loss_condition_stops_game_and_shows_message
test_win_condition_stops_game_and_shows_message
test_game_continues_when_active
Objective: Create a script that initializes the game screen, a Paddle object, and a Ball object. Implement the main game loop where the paddle can be moved by the player and the ball bounces off the screen edges and the paddle.
Core Concept: Assembling multiple objects in a main game loop.
This quest involves bringing together the basic components of the game: the screen, the player's paddle, and the ball. You will set up the main game loop structure and implement the fundamental interactions between the player input, the paddle, the ball, and the screen boundaries.
Guidance:
Initialization: The main.py
skeleton already initializes Pygame and sets up the screen and clock. It also creates instances of the Paddle
and Ball
classes.
Paddle Movement:
move
method of the paddle
object. The move
method should take an argument indicating the direction (e.g., -1 for left, 1 for right).Paddle.move
method, update the paddle's rect.x
based on the direction and the paddle's speed.Paddle.move
to ensure the paddle's rect.left
does not go below 0 and its rect.right
does not exceed the screen width.# Example: Getting pressed keys (use this in main loop)
keys = pygame.key.get_pressed()
# Example: Checking a specific key (use this with the keys variable)
if keys[pygame.K_LEFT]:
# Call paddle move method here
pass
# Example: Updating a rect's position
self.rect.x += self.speed
# Example: Boundary check
if self.rect.left < 0:
self.rect.left = 0
Ball Movement:
move
method of the ball
object.Ball.move
method, update the ball's rect.x
and rect.y
based on its speed_x
and speed_y
.# Example: Updating ball position (use in Ball.move)
self.rect.x += self.speed_x
self.rect.y += self.speed_y
Wall Collision (Ball):
Ball.move
method, after updating the position, check if the ball's rect
has hit the screen boundaries (left, right, and top).speed_x
).speed_y
). (Note: The bottom wall collision will be handled later for game over).# Example: Checking wall collision (use in Ball.move)
if self.rect.left <= 0 or self.rect.right >= self.screen_width:
# Reverse horizontal speed here
pass
if self.rect.top <= 0:
# Reverse vertical speed here
pass
# Example: Reversing speed
self.speed_x *= -1
Paddle Collision (Ball):
check_collision
method of the ball
object, passing the paddle's rect
.Ball.check_collision
method, use ball.rect.colliderect(paddle_rect)
to detect if the ball and paddle rectangles overlap.speed_y > 0
), reverse the ball's vertical speed (speed_y
). This prevents the ball from getting stuck inside the paddle or bouncing incorrectly when hitting the bottom or sides of the paddle.# Example: Checking collision between two rects (use in Ball.check_collision)
if self.rect.colliderect(other_rect):
# Handle collision here
pass
# Example: Checking ball direction
if self.speed_y > 0:
# Reverse vertical speed here
pass
Drawing: The drawing code is already in place in the main loop. It clears the screen and then calls the draw
methods of the paddle and ball.
(Image/Gif Suggestion: Show the paddle moving left and right. Show the ball bouncing off the walls and the paddle.)
Objective: Create a script that initializes a list of Brick objects arranged in a grid. Implement collision detection so that the ball destroys a brick upon impact and reverses its vertical direction.
Core Concept: Managing and iterating through a list of objects for collision.
This quest introduces multiple instances of an object (bricks) and requires managing them in a collection (a list). You will learn how to create these objects in a structured way and how to check for interactions between a single object (the ball) and multiple objects in a list.
Guidance:
Create Bricks:
create_bricks()
function in main.py
.for
loops to iterate through the desired number of rows and columns (BRICK_ROWS
, BRICK_COLS
).x
and y
position for each brick based on the constants (BRICK_OFFSET_LEFT
, BRICK_OFFSET_TOP
, BRICK_WIDTH
, BRICK_HEIGHT
, BRICK_PADDING
).Brick
instance for each position and append it to the bricks
list.# Example: Nested loops for grid
bricks = []
for row in range(BRICK_ROWS):
for col in range(BRICK_COLS):
# Calculate x, y here
brick = Brick(x, y, BRICK_WIDTH, BRICK_HEIGHT, BLUE)
bricks.append(brick)
return bricks
# Example: Calculating position in a grid
x = offset_left + col * (item_width + padding)
y = offset_top + row * (item_height + padding)
Handle Ball-Brick Collision:
handle_ball_brick_collision(ball, bricks)
function in main.py
.bricks
list.ball.rect
and the current brick.rect
using colliderect
.ball.reverse_y_direction()
).bricks
list.break
statement to exit the loop immediately after finding the first collision. This is important because a single ball movement might overlap with multiple bricks, but you typically only want to process one collision per frame to avoid double-bounces or incorrect behavior.# Example: Iterating through a list and checking collision
for item in item_list:
if object.rect.colliderect(item.rect):
# Handle collision with item
# Remove item from list
# Stop checking (break)
pass
# Example: Removing an item from a list
my_list.remove(item_to_remove)
Integration: The main()
function already calls create_bricks()
to get the initial list and handle_ball_brick_collision()
inside the game loop. The drawing loop iterates through the bricks
list to draw them.
(Image/Gif Suggestion: Show a grid of bricks appearing. Show the ball hitting a brick, the brick disappearing, and the ball bouncing back.)
Objective: Create a script that implements win and loss conditions using a simple boolean flag (e.g., game_active
). The game should stop if the ball goes off the bottom of the screen, and a 'You Win!' message should appear if all bricks are destroyed.
Core Concept: Basic game state management with boolean flags.
This quest introduces the concept of game state. Instead of the game always running the same logic, it will now check if it's in an "active" state (playing) or an "inactive" state (game over or win). This simple state management controls what happens in the game loop.
Guidance:
Game State Flag: A boolean variable game_active
is provided in the run_breakout_game
function, initialized to True
. An end_message
string is also provided.
Control Game Logic: The skeleton already has an if game_active:
block. Place all the game logic that should only run when the game is being played (ball movement, wall collision, brick collision) inside this block.
Loss Condition:
if game_active:
block, after the ball movement and collision checks, add a check to see if the ball's rect.bottom
is greater than or equal to the SCREEN_HEIGHT
.game_active
to False
and set end_message
to "Game Over".# Example: Checking if ball is off the bottom
if ball.rect.bottom >= SCREEN_HEIGHT:
# Set game_active to False
# Set end_message
pass
Win Condition:
if game_active:
block, after handling brick collisions, check if the bricks
list is empty (if not bricks:
).game_active
to False
and set end_message
to "You Win!".# Example: Checking if a list is empty
if not my_list:
# Set game_active to False
# Set end_message
pass
Drawing Based on State: The skeleton already has an else
block corresponding to if game_active:
.
if game_active:
block, draw the game elements (ball, bricks).else
block (when game_active
is False
), the code to draw the end_message
using Pygame fonts is already provided.(Image/Gif Suggestion: Show the ball going off the bottom and the "Game Over" message appearing. Show the last brick being broken and the "You Win!" message appearing.)
Objective: Create a script that defines a GameStateManager
class. The class should be initialized with a state (e.g., 'PLAYING') and have methods to change the state (e.g., change_state('GAME_OVER')
) and check the current state (e.g., is_state('PLAYING')
).
Core Concept: Encapsulation with a dedicated state management class.
Instead of using a simple boolean flag in the main game function, you will create a dedicated class to manage the game's state. This is a step towards better organization and encapsulation, making the main game logic cleaner and the state management reusable.
Guidance:
Define the Class: Create a class named GameStateManager
in game_state_manager.py
.
__init__
Method:
__init__
method. It should accept one argument, initial_state
.__init__
, create an instance variable (e.g., self.current_state
) and assign the initial_state
argument to it.# Example: Class initialization
class MyClass:
def __init__(self, initial_value):
self.my_attribute = initial_value
change_state
Method:
change_state
. It should accept one argument, new_state
.change_state
, update the self.current_state
instance variable to the value of new_state
.# Example: Method to change an attribute
def set_attribute(self, new_value):
self.my_attribute = new_value
is_state
Method:
is_state
. It should accept one argument, state_to_check
.is_state
, compare self.current_state
with state_to_check
.True
if they match, False
otherwise).# Example: Method to check an attribute's value
def check_attribute(self, value_to_check):
return self.my_attribute == value_to_check
Testing in main.py
: The create_and_test_gsm()
function in main.py
is provided to demonstrate the class.
create_and_test_gsm()
, create an instance of your GameStateManager
, initializing it with 'PLAYING'.change_state
method to change the state to 'GAME_OVER'.is_state
would be used.(Image/Gif Suggestion: A simple diagram showing a box representing the GameStateManager with arrows indicating state transitions like 'PLAYING' -> 'GAME_OVER' or 'PLAYING' -> 'VICTORY'.)
Objective: Refactor the game from the previous quest. Replace the simple boolean flag with an instance of the GameStateManager
. The game's flow (updating and drawing game elements) should now be controlled by checking the state via the manager (e.g., if manager.is_state('PLAYING'):
).
Core Concept: Refactoring to use a state manager object.
This quest applies the GameStateManager
class you created. You will modify the main game loop to use the manager object to determine whether game logic and drawing should occur, replacing the previous boolean flag logic.
Guidance:
Instantiate the Manager: In the run_game
function in main.py
, create an instance of the GameStateManager
class, initializing it with the state 'PLAYING'. This replaces the game_active
boolean flag.
Control Update Logic: Find the section in the main loop where game objects (like the player and ball) are updated. Wrap the calls to their update()
methods inside an if
statement that checks if the game_state_manager
is currently in the 'PLAYING' state using the is_state()
method.
# Example: Controlling updates based on state
if game_state_manager.is_state('PLAYING'):
# Call update methods here
player.update()
ball.update()
Control Drawing Logic: Find the section in the main loop where game objects are drawn. Wrap the calls to their draw()
methods inside an if
statement that checks if the game_state_manager
is currently in the 'PLAYING' state using the is_state()
method.
# Example: Controlling drawing based on state
if game_state_manager.is_state('PLAYING'):
# Call draw methods here
player.draw(screen)
ball.draw(screen)
Remove Old Logic: Ensure you remove any remaining code that relied on the old boolean game_active
flag for controlling the main game logic and drawing sections.
(Image/Gif Suggestion: Show the game running normally when in the 'PLAYING' state. Show the screen remaining static or displaying a different message when the state is not 'PLAYING', demonstrating that updates and drawing are skipped.)
Objective: Assemble the complete Arkanoid game. Integrate a scoring system and a lives counter. Update the score when a brick is broken and decrease lives when the ball is missed. The GameStateManager
should now handle transitions to 'GAME_OVER' or 'VICTORY' states.
Core Concept: Integrating UI elements with game state.
This is the final assembly quest. You will bring together all the components developed in previous quests (Paddle, Ball, Bricks, GameStateManager, UI) to create a functional Arkanoid game. You will integrate scoring and lives, and use the GameStateManager
to control the flow and display based on win/loss conditions.
Guidance:
Review Components: Ensure you have the Paddle
, Ball
, Brick
, BrickManager
, GameStateManager
, and UIManager
classes, along with the config.py
file. The Game
class in main.py
orchestrates these.
Input Handling (_handle_input
):
_handle_input
method of the Game
class, get the state of all keyboard keys using pygame.key.get_pressed()
.is_playing()
, check for pygame.K_LEFT
and pygame.K_RIGHT
.self.paddle.move(-1)
for left and self.paddle.move(1)
for right.# Example: Handling input based on state
if self.game_state_manager.is_playing():
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
# Move paddle left
pass
# Check for right key and move paddle right
Game State Updates (_update_game_state
):
self.game_state_manager.is_playing()
. The skeleton already includes this check.self.ball.move()
.self.ball.rect
boundaries and reversing self.ball.speed_x
or self.ball.speed_y
.self.ball.rect.colliderect(self.paddle.rect)
. If they collide and self.ball.speed_y > 0
, reverse self.ball.speed_y
.brick_broken = self.brick_manager.check_collision(self.ball)
. If brick_broken
is True
, call self.game_state_manager.add_score()
and reverse self.ball.speed_y
.self.ball.rect.bottom >= SCREEN_HEIGHT
. If true, call self.game_state_manager.lose_life()
, self.ball.reset()
, and self.paddle.reset()
.self.game_state_manager.check_victory(self.brick_manager.bricks)
.# Example: Checking ball bottom boundary
if self.ball.rect.bottom >= SCREEN_HEIGHT:
# Lose a life
# Reset ball
# Reset paddle
pass
# Example: Checking for victory
self.game_state_manager.check_victory(self.brick_manager.bricks)
Drawing Elements (_draw_elements
):
if/elif/else
statements based on the self.game_state_manager.is_playing()
, is_game_over()
, and is_victory()
methods.is_playing()
, draw self.paddle
, self.ball
, and self.brick_manager
.is_game_over()
, call self.ui_manager.draw_game_over()
.is_victory()
, call self.ui_manager.draw_victory()
.self.ui_manager.draw_hud()
to display the score and lives, regardless of the main game state.# Example: Drawing based on state
if self.game_state_manager.is_playing():
# Draw game objects
pass
elif self.game_state_manager.is_game_over():
# Draw game over screen
pass
elif self.game_state_manager.is_victory():
# Draw victory screen
pass
# Always draw HUD
self.ui_manager.draw_hud(self.game_state_manager.score, self.game_state_manager.lives)
(Image/Gif Suggestion: Show the complete game running, including the HUD, bricks breaking, score updating, lives decreasing, and the final win/loss screens.)