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!