Making Games Replayable

Introduction: Beyond the Single Run

Currently, our game has a major limitation: once you collide with an obstacle, the program simply closes. In game development, we call this a "one-and-done" loop. To make our game feel professional, we need to handle failure gracefully.

In this lesson, we will implement a Finite State Machine. This sounds complex, but it's just a fancy way of saying our game can be in different modes: a start screen, active gameplay, and a "Game Over" screen. Our goal is to create a seamless loop where a player can lose, see their score, and jump right back into the action.

Understanding Phase State vs. Session Data

It's important to distinguish between where the player is in the game menu (State) and what is happening in the game (Session Data).

  • game_state: This is our compass. It tells the game loop whether to move characters or just draw text on a menu.
  • Session Data: These are variables like player position, score, and obstacle_speed.

When a player hits "Restart," we don't just change the game_state string to "playing". If we did that without cleaning up our data, the player would immediately collide with the same obstacle again! We must explicitly reset the session data to give the player a fresh start.

Transforming the Collision Response

Previously, we used running = False to kill the program on collision. Now, we want the heartbeat of the game to keep thumping, but we want the action to freeze.

We achieve this by swapping the state:

Python
for obstacle in obstacles:
    if player.colliderect(obstacle):
        hit_sound.play()
        game_state = "game_over" # The magic switch
        break

By changing the state to "game_over", the code inside our elif game_state == "playing": block will stop running on the next frame. The obstacles will stop moving, the player will freeze, and we can now draw a dedicated Game Over UI.

Designing the Game Over Screen

A good Game Over screen does two things: it provides feedback (the final score) and clear instructions (how to try again).

Inside our main loop, we add a new branch:

Python
        elif game_state == "game_over":
            screen.fill(DARK_BLUE)
            # Center "Game Over" near the top
            game_over_text = title_font.render("Game Over", True, WHITE)
            screen.blit(game_over_text, game_over_text.get_rect(center=(WIDTH // 2, 180)))
            
            # Show how well they did!
            final_score_text = font.render(f"Final Score: {score}", True, WHITE)
            screen.blit(final_score_text, final_score_text.get_rect(center=(WIDTH // 2, 300)))

Using get_rect(center=(...)) is a life-saver here. It automatically calculates the width of your text and centers it perfectly on the screen regardless of how many digits the score has.

The Power of a Reset Helper

When the player presses Enter to restart, we need to perform several tasks: move the player back to the starting line, clear the old obstacles, spawn new ones, and set the score back to zero. Doing this directly inside our event loop would make it messy.

Instead, we create a reset_game() function:

Python
def reset_game(player, obstacles):
    # 1. Reset position
    player.x, player.y = 375, 540
    # 2. Clear old obstacles and spawn fresh ones
    obstacles.clear()
    obstacles.extend(spawn_obstacles(5))
    # 3. Reset stats
    return 0, 4 # New score and initial speed

Pro-tip: Notice that we modify player and obstacles directly. Because they are objects (a Rect and a list), Python passes them by reference, meaning changes inside the function affect the original variables!

Wiring it All Together

In our event handling section, we now differentiate what "Enter" means based on our current state:

Python
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_RETURN:
                    if game_state == "start":
                        game_state = "playing"
                    elif game_state == "game_over":
                        # Cleanup the old run before starting a new one
                        score, obstacle_speed = reset_game(player, obstacles)
                        game_state = "playing"

Conclusion

By adding a Game Over state and a reset function, you've moved from a simple script to a real "game loop." Your game can now be played indefinitely, which is the foundation of every addictive arcade title. Next up, we'll add some atmosphere with background music!

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal