Contents

In this tutorial, we’ll build a complete Pong game using Pygame with intelligent rule-based AI opponents. The game features two modes: Player vs Machine and Machine vs Machine, with sophisticated ball prediction algorithms and multiple difficulty levels.

Features

  • Two Game Modes: Player vs Machine and Machine vs Machine
  • Intelligent AI: Rule-based machine with ball trajectory prediction
  • Multiple Difficulty Levels: Progressive difficulty from level 1 to 10
  • Sound Effects: Procedurally generated bounce sounds using NumPy
  • Smooth Gameplay: 60 FPS with physics-based ball movement
  • Visual Polish: Rounded paddles, dashed center line, and score display

Prerequisites

pip install pygame numpy

Complete Code

import pygame
import sys
import random
import math
import numpy as np

pygame.init()
pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=512)

WIDTH, HEIGHT = 640, 480
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("PyShine - Pong [Press M Key to Change Mode]")

clock = pygame.time.Clock()
font = pygame.font.SysFont("Arial", 36)
small_font = pygame.font.SysFont("Arial", 18)

PADDLE_WIDTH = 15
PADDLE_HEIGHT = 100
BALL_SIZE = 15
PADDLE_SPEED = 7
BALL_SPEED_INIT = 6

def generate_bounce_sound():
    sample_rate = 44100
    duration = 0.05
    frequency = 600
    n_samples = int(sample_rate * duration)
    t = np.linspace(0, duration, n_samples, False)
    wave = np.sin(2 * np.pi * frequency * t)
    envelope = np.exp(-t * 40)
    wave = wave * envelope
    wave = (wave * 0.4 * 32767).astype(np.int16)
    stereo_wave = np.column_stack((wave, wave))
    return pygame.sndarray.make_sound(stereo_wave)

try:
    bounce_sound = generate_bounce_sound()
    sounds_enabled = True
except:
    sounds_enabled = False

def play_bounce():
    if sounds_enabled:
        try:
            bounce_sound.play()
        except:
            pass

class RuleBasedMachine:
    def __init__(self, name="Machine", difficulty=0.7, prediction_strength=0.5):
        self.name = name
        self.difficulty = difficulty
        self.prediction_strength = prediction_strength
        self.reaction_delay = 0
        self.target_y = HEIGHT // 2
    
    def predict_ball_y(self, ball_x, ball_y, ball_vx, ball_vy, target_x):
        if ball_vx == 0:
            return ball_y
        
        time_to_reach = (target_x - ball_x) / ball_vx
        predicted_y = ball_y + ball_vy * time_to_reach
        
        bounces = 0
        while predicted_y < 0 or predicted_y > HEIGHT:
            if predicted_y < 0:
                predicted_y = -predicted_y
                bounces += 1
            elif predicted_y > HEIGHT:
                predicted_y = 2 * HEIGHT - predicted_y
                bounces += 1
            if bounces > 10:
                break
        
        return predicted_y
    
    def decide(self, ball_x, ball_y, ball_vx, ball_vy, paddle_y, is_left=True):
        if random.random() > self.difficulty:
            return 1
        
        if is_left:
            if ball_vx < 0:
                target_x = 35
                predicted_y = self.predict_ball_y(ball_x, ball_y, ball_vx, ball_vy, target_x)
                predicted_y = ball_y + (predicted_y - ball_y) * self.prediction_strength
            else:
                predicted_y = HEIGHT // 2
        else:
            if ball_vx > 0:
                target_x = WIDTH - 35
                predicted_y = self.predict_ball_y(ball_x, ball_y, ball_vx, ball_vy, target_x)
                predicted_y = ball_y + (predicted_y - ball_y) * self.prediction_strength
            else:
                predicted_y = HEIGHT // 2
        
        self.target_y = predicted_y
        paddle_center = paddle_y + PADDLE_HEIGHT / 2
        dead_zone = 10
        
        if paddle_center < predicted_y - dead_zone:
            return 2
        elif paddle_center > predicted_y + dead_zone:
            return 0
        else:
            return 1

player_score = 0
ai_score = 0
level = 1
max_score = 5
max_level = 10

player_y = HEIGHT // 2 - PADDLE_HEIGHT // 2
ai_y = HEIGHT // 2 - PADDLE_HEIGHT // 2

ball_x = WIDTH // 2
ball_y = HEIGHT // 2
ball_vx = BALL_SPEED_INIT * random.choice([-1, 1])
ball_vy = BALL_SPEED_INIT * random.choice([-1, 1])

left_machine = RuleBasedMachine("Machine 1", difficulty=0.85, prediction_strength=0.8)
right_machine = RuleBasedMachine("Machine 2", difficulty=0.85, prediction_strength=0.8)

game_mode = 0
MODES = ["Player vs Machine", "Machine vs Machine"]

game_over = False
winner = ""

def reset_ball(direction=0):
    global ball_x, ball_y, ball_vx, ball_vy
    ball_x = WIDTH // 2
    ball_y = HEIGHT // 2
    speed = BALL_SPEED_INIT + (level - 1) * 0.5
    if direction == 0:
        direction = random.choice([-1, 1])
    ball_vx = speed * direction
    ball_vy = speed * random.choice([-1, 1])

def draw_paddle(x, y, color):
    pygame.draw.rect(screen, color, (x, y, PADDLE_WIDTH, PADDLE_HEIGHT), border_radius=5)
    pygame.draw.rect(screen, (255, 255, 255), (x, y, PADDLE_WIDTH, PADDLE_HEIGHT), 2, border_radius=5)

def draw_ball(x, y):
    pygame.draw.circle(screen, (255, 255, 255), (int(x), int(y)), BALL_SIZE)
    pygame.draw.circle(screen, (200, 200, 200), (int(x), int(y)), BALL_SIZE, 2)

def draw_dashed_line():
    for y in range(0, HEIGHT, 30):
        pygame.draw.rect(screen, (100, 100, 100), (WIDTH // 2 - 2, y, 4, 15))

def draw_scores():
    player_text = font.render(str(player_score), True, (100, 200, 255))
    ai_text = font.render(str(ai_score), True, (255, 100, 100))
    screen.blit(player_text, (WIDTH // 4 - 20, 30))
    screen.blit(ai_text, (3 * WIDTH // 4 - 20, 30))

def draw_machine_labels():
    label_surface = pygame.Surface((150, 30), pygame.SRCALPHA)
    label_surface.fill((0, 0, 0, 0))
    
    if game_mode == 0:
        left_label = small_font.render("Player", True, (100, 200, 255, 180))
        right_label = small_font.render("Machine", True, (255, 100, 100, 180))
    else:
        left_label = small_font.render("Machine 1", True, (100, 200, 255, 180))
        right_label = small_font.render("Machine 2", True, (255, 100, 100, 180))
    
    screen.blit(left_label, (20, HEIGHT - 30))
    screen.blit(right_label, (WIDTH - 100, HEIGHT - 30))

def draw_mode_indicator():
    mode_text = font.render(MODES[game_mode], True, (255, 255, 100))
    screen.blit(mode_text, (WIDTH // 2 - mode_text.get_width() // 2, HEIGHT - 55))

def draw_game_over():
    overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
    overlay.fill((0, 0, 0, 180))
    screen.blit(overlay, (0, 0))
    
    winner_text = font.render(winner, True, (255, 255, 100))
    screen.blit(winner_text, (WIDTH // 2 - winner_text.get_width() // 2, HEIGHT // 2 - 50))
    
    restart_text = small_font.render("SPACE: Restart | M: Change Mode | ESC: Quit", True, (200, 200, 200))
    screen.blit(restart_text, (WIDTH // 2 - restart_text.get_width() // 2, HEIGHT // 2 + 20))

def update_left_machine():
    global player_y
    
    action = left_machine.decide(ball_x, ball_y, ball_vx, ball_vy, player_y, is_left=True)
    
    if action == 0:
        player_y -= PADDLE_SPEED
    elif action == 2:
        player_y += PADDLE_SPEED
    
    player_y = max(0, min(HEIGHT - PADDLE_HEIGHT, player_y))

def update_right_machine():
    global ai_y
    
    action = right_machine.decide(ball_x, ball_y, ball_vx, ball_vy, ai_y, is_left=False)
    
    if action == 0:
        ai_y -= PADDLE_SPEED
    elif action == 2:
        ai_y += PADDLE_SPEED
    
    ai_y = max(0, min(HEIGHT - PADDLE_HEIGHT, ai_y))

def update_ball():
    global ball_x, ball_y, ball_vx, ball_vy, player_score, ai_score, game_over, winner, level
    
    ball_x += ball_vx
    ball_y += ball_vy
    
    if ball_y - BALL_SIZE <= 0 or ball_y + BALL_SIZE >= HEIGHT:
        ball_vy = -ball_vy
        ball_y = max(BALL_SIZE, min(HEIGHT - BALL_SIZE, ball_y))
        play_bounce()
    
    if ball_x - BALL_SIZE <= PADDLE_WIDTH + 20:
        if player_y <= ball_y <= player_y + PADDLE_HEIGHT:
            ball_vx = abs(ball_vx) * 1.02
            relative_intersect = (player_y + PADDLE_HEIGHT / 2) - ball_y
            normalized = relative_intersect / (PADDLE_HEIGHT / 2)
            ball_vy = -normalized * abs(ball_vx) * 0.8
            ball_x = PADDLE_WIDTH + 20 + BALL_SIZE
            play_bounce()
    
    if ball_x + BALL_SIZE >= WIDTH - PADDLE_WIDTH - 20:
        if ai_y <= ball_y <= ai_y + PADDLE_HEIGHT:
            ball_vx = -abs(ball_vx) * 1.02
            relative_intersect = (ai_y + PADDLE_HEIGHT / 2) - ball_y
            normalized = relative_intersect / (PADDLE_HEIGHT / 2)
            ball_vy = -normalized * abs(ball_vx) * 0.8
            ball_x = WIDTH - PADDLE_WIDTH - 20 - BALL_SIZE
            play_bounce()
    
    if ball_x < 0:
        ai_score += 1
        if ai_score >= max_score:
            game_over = True
            if game_mode == 1:
                winner = "Machine 2 Wins!"
            else:
                winner = "Machine Wins!"
        else:
            reset_ball(1)
    
    if ball_x > WIDTH:
        player_score += 1
        if player_score >= max_score:
            if level < max_level and game_mode == 0:
                level += 1
                player_score = 0
                ai_score = 0
                reset_ball(-1)
            else:
                game_over = True
                if game_mode == 1:
                    winner = "Machine 1 Wins!"
                else:
                    winner = "You Win All Levels!"
        else:
            reset_ball(-1)

def reset_game():
    global player_score, ai_score, level, game_over, winner, player_y, ai_y
    player_score = 0
    ai_score = 0
    level = 1
    game_over = False
    winner = ""
    player_y = HEIGHT // 2 - PADDLE_HEIGHT // 2
    ai_y = HEIGHT // 2 - PADDLE_HEIGHT // 2
    reset_ball()

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                running = False
            if event.key == pygame.K_SPACE and game_over:
                reset_game()
            if event.key == pygame.K_m:
                game_mode = (game_mode + 1) % len(MODES)
                reset_game()
    
    if not game_over:
        if game_mode == 0:
            keys = pygame.key.get_pressed()
            if keys[pygame.K_UP] or keys[pygame.K_w]:
                player_y -= PADDLE_SPEED
            if keys[pygame.K_DOWN] or keys[pygame.K_s]:
                player_y += PADDLE_SPEED
            player_y = max(0, min(HEIGHT - PADDLE_HEIGHT, player_y))
            update_right_machine()
        elif game_mode == 1:
            update_left_machine()
            update_right_machine()
        
        update_ball()
    
    screen.fill((20, 25, 40))
    
    draw_dashed_line()
    draw_paddle(20, player_y, (100, 200, 255))
    draw_paddle(WIDTH - PADDLE_WIDTH - 20, ai_y, (255, 100, 100))
    draw_ball(ball_x, ball_y)
    draw_scores()
    draw_machine_labels()
    draw_mode_indicator()
    
    if game_over:
        draw_game_over()
    
    pygame.display.flip()
    clock.tick(60)

pygame.quit()
sys.exit()

Key Components Explained

1. Sound Generation

The game uses NumPy to procedurally generate bounce sounds:

def generate_bounce_sound():
    sample_rate = 44100
    duration = 0.05
    frequency = 600
    n_samples = int(sample_rate * duration)
    t = np.linspace(0, duration, n_samples, False)
    wave = np.sin(2 * np.pi * frequency * t)
    envelope = np.exp(-t * 40)
    wave = wave * envelope
    wave = (wave * 0.4 * 32767).astype(np.int16)
    stereo_wave = np.column_stack((wave, wave))
    return pygame.sndarray.make_sound(stereo_wave)

This creates a sine wave with an exponential decay envelope for a realistic bounce sound.

2. Rule-Based AI

The AI uses ball trajectory prediction with wall bounce calculations:

def predict_ball_y(self, ball_x, ball_y, ball_vx, ball_vy, target_x):
    if ball_vx == 0:
        return ball_y
    
    time_to_reach = (target_x - ball_x) / ball_vx
    predicted_y = ball_y + ball_vy * time_to_reach
    
    bounces = 0
    while predicted_y < 0 or predicted_y > HEIGHT:
        if predicted_y < 0:
            predicted_y = -predicted_y
            bounces += 1
        elif predicted_y > HEIGHT:
            predicted_y = 2 * HEIGHT - predicted_y
            bounces += 1
        if bounces > 10:
            break
    
    return predicted_y

The AI predicts where the ball will be when it reaches the paddle, accounting for wall bounces.

3. Physics-Based Ball Movement

Ball speed increases with each paddle hit:

if ball_x - BALL_SIZE <= PADDLE_WIDTH + 20:
    if player_y <= ball_y <= player_y + PADDLE_HEIGHT:
        ball_vx = abs(ball_vx) * 1.02
        relative_intersect = (player_y + PADDLE_HEIGHT / 2) - ball_y
        normalized = relative_intersect / (PADDLE_HEIGHT / 2)
        ball_vy = -normalized * abs(ball_vx) * 0.8
        ball_x = PADDLE_WIDTH + 20 + BALL_SIZE
        play_bounce()

The ball angle changes based on where it hits the paddle, adding strategic depth.

4. Multiple Game Modes

The game supports two modes:

  • Player vs Machine: You control the left paddle with arrow keys or W/S
  • Machine vs Machine: Watch two AI opponents compete

Press M to switch between modes.

Controls

  • Arrow Keys / W, S: Move paddle up/down
  • M: Change game mode
  • SPACE: Restart game (when game over)
  • ESC: Quit

Difficulty Progression

In Player vs Machine mode, the game has 10 levels. Each level increases:

  • Ball speed
  • AI prediction accuracy
  • AI reaction time

Win all 10 levels to complete the game!

Customization

You can easily customize the game by modifying these constants:

PADDLE_WIDTH = 15
PADDLE_HEIGHT = 100
BALL_SIZE = 15
PADDLE_SPEED = 7
BALL_SPEED_INIT = 6

Or adjust AI difficulty:

left_machine = RuleBasedMachine("Machine 1", difficulty=0.85, prediction_strength=0.8)
right_machine = RuleBasedMachine("Machine 2", difficulty=0.85, prediction_strength=0.8)
  • difficulty: Probability of making correct decisions (0.0 to 1.0)
  • prediction_strength: How much to trust predictions (0.0 to 1.0)

Conclusion

This Pong game demonstrates several important game development concepts:

  • Procedural Sound Generation: Creating sounds without external files
  • Rule-Based AI: Predictive algorithms for game opponents
  • Physics Simulation: Realistic ball movement and collision
  • State Management: Handling game modes, levels, and game over states
  • Visual Polish: Professional-looking graphics with Pygame

The code is well-structured and easy to extend. You could add features like:

  • Power-ups and special abilities
  • Multiplayer networking
  • Different AI strategies
  • Particle effects
  • High score tracking

Happy coding!