Contents
Advanced Snake Game with Sound Effects and Boost Mechanic
This tutorial covers building a feature-rich Snake game in Python using Tkinter.It goes beyond the basics and includes:
- Procedural sound generation (no external audio files needed)
- Boost mechanic for speed control
- Gradient snake rendering with eyes
- Food timer system
- High score persistence
Requirements
- Python 3.8+
- Tkinter (comes preinstalled with Python)
- Pygame (optional, for sound playback)
Run the game using:
python snake.py
Game Features
- Arrow keys / WASD for movement
- Boost mechanic: Hold key for 0.3s to speed up
- Procedural sound effects: Eat, move, game over
- Gradient snake colors: Head → body → tail
- Directional eyes: Snake looks where it’s going
- Food timer: Food respawns every 10 seconds
- High score: Saved to JSON file
- Game over screen: Shows score and restart option
Procedural Sound Generation
One of the coolest features is that the game generates its own sound files using Python’s wave module — no external audio files needed!
Generating WAV Files
import wave
import struct
import math
def generate_wav_file(filename, frequency, duration, volume=0.5):
sample_rate = 44100
n_samples = int(sample_rate * duration)
samples = []
for i in range(n_samples):
t = i / sample_rate
envelope = math.sin(math.pi * t / duration)
sample = volume * envelope * math.sin(2 * math.pi * frequency * t)
samples.append(int(sample * 32767))
with wave.open(filepath, 'w') as wav_file:
wav_file.setnchannels(1)
wav_file.setsampwidth(2)
wav_file.setframerate(sample_rate)
for sample in samples:
wav_file.writeframes(struct.pack('<h', sample))
What’s happening:
- Creates a sine wave at the specified frequency
- Applies an envelope (fade in/out) using
sin() - Converts to 16-bit PCM audio format
- Writes to a WAV file
Sound Effects
The game generates three distinct sounds:
| Sound | Frequency | Duration | Purpose |
|---|---|---|---|
| Eat | 800-1200Hz sweep | 0.15s | Food collection |
| Move | 600Hz | 0.05s | Direction change |
| Game Over | 400-200Hz drop | 0.8s | Collision |
Why procedural audio?
- No external files to manage
- Sounds are always available
- Easy to customize frequencies and durations
- Perfect for distribution
Boost Mechanic
The game includes a boost feature that rewards skilled players:
def check_boost(self):
if self.key_pressed and not self.game_over:
held_time = time.time() - self.key_press_time
if held_time > 0.3:
self.is_boosting = True
else:
self.is_boosting = False
else:
self.is_boosting = False
How it works:
- Player holds a movement key
- After 0.3 seconds, boost activates
- Snake moves 8x faster during boost
- Speed gradually increases as you eat food
Why this matters:
- Adds skill expression
- Risk/reward: Boost through tight spaces
- Makes the game more engaging
Gradient Snake Rendering
Instead of a solid color, the snake uses a gradient from head to tail:
def interpolate_color(self, color1, color2, factor):
r1, g1, b1 = int(color1[1:3], 16), ...
r2, g2, b2 = int(color2[1:3], 16), ...
r = int(r1 + (r2 - r1) * factor)
g = int(g1 + (g2 - g1) * factor)
b = int(b1 + (b2 - b1) * factor)
return f"#{r:02x}{g:02x}{b:02x}"
Visual effect:
- Head: Bright green (
#33b233) - Body: Gradient to tail
- Tail: Darker green (
#1f6b1f) - Size also decreases from head to tail
This creates a smooth, organic look for the snake.
Directional Snake Eyes
The snake’s eyes follow the direction of movement:
def draw_snake_eyes(self, x, y, size):
dx, dy = self.direction
if dx == 1: # Moving right
eye_positions = [(cx + 2, cy - offset), (cx + 2, cy + offset)]
elif dx == -1: # Moving left
eye_positions = [(cx - 2, cy - offset), (cx - 2, cy + offset)]
# ... up and down cases
Why this matters:
- Makes the snake feel alive and responsive
- Players can instantly see direction
- Adds personality to the game
Food Timer System
Food doesn’t stay forever — it respawns every 10 seconds:
FOOD_TIMER = 10 # seconds
def check_food_timer(self):
if self.food:
elapsed = time.time() - self.food_spawn_time
if elapsed >= FOOD_TIMER:
self.spawn_food()
Visual countdown: The food shows a countdown timer:
- 0-3 seconds: Black (urgent)
- 4-5 seconds: Orange (warning)
- 6-10 seconds: Red (normal)
This creates urgency and keeps players moving.
High Score Persistence
The game saves your high score to a JSON file:
SCORE_FILE = "highscore.json"
def save_high_score(self):
if self.score > self.high_score:
self.high_score = self.score
with open(score_path, 'w') as f:
json.dump({'high_score': self.high_score}, f)
Why JSON?
- Human-readable
- Easy to parse and modify
- No database required
- Works across platforms
Complete Code Structure
class SnakeGame:
def __init__(self, root):
self.load_high_score()
self.load_sounds()
self.reset_game()
self.game_loop()
def load_sounds(self):
self.sounds = {}
if SOUND_ENABLED:
self.sounds[name] = pygame.mixer.Sound(path)
def play_sound(self, name):
if SOUND_ENABLED and name in self.sounds:
self.sounds[name].play()
def move_snake(self):
self.direction = self.next_direction
new_head = self.wrap_position(head_x + dx, head_y + dy)
if new_head in self.snake:
self.game_over = True
self.play_sound('game_over')
self.save_high_score()
self.snake.insert(0, new_head)
if new_head == self.food:
self.score += 10
self.spawn_food()
self.play_sound('eat')
if self.speed > MIN_SPEED:
self.speed -= SPEED_INCREMENT
Key Takeaways
- Procedural audio eliminates external file dependencies
- Gradient rendering creates polished visuals
- Boost mechanics add depth to gameplay
- Directional eyes make the game feel responsive
- Food timers create urgency and prevent camping
- JSON persistence is simple and cross-platform
Running the Game
# First time - generates sound files
python snake.py
# Subsequent runs - uses existing sounds
python snake.py
The game will automatically:
- Generate missing sound files
- Load existing sounds if they exist
- Save your high score
- Display “NEW HIGH SCORE!” when you beat your record
Next Steps
Try modifying the game:
- Change the boost multiplier (currently 8x)
- Adjust the food timer (currently 10 seconds)
- Modify the color scheme (edit the hex codes)
- Add obstacles or power-ups
- Implement multiplayer (local or network)
Full Source Code
import tkinter as tk
import random
import os
import sys
import json
import time
import wave
import struct
import math
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
def generate_wav_file(filename, frequency, duration, volume=0.5):
sample_rate = 44100
n_samples = int(sample_rate * duration)
samples = []
for i in range(n_samples):
t = i / sample_rate
envelope = math.sin(math.pi * t / duration)
sample = volume * envelope * math.sin(2 * math.pi * frequency * t)
samples.append(int(sample * 32767))
filepath = os.path.join(SCRIPT_DIR, filename)
with wave.open(filepath, 'w') as wav_file:
wav_file.setnchannels(1)
wav_file.setsampwidth(2)
wav_file.setframerate(sample_rate)
for sample in samples:
wav_file.writeframes(struct.pack('<h', sample))
return filepath
def generate_eat_sound():
filepath = os.path.join(SCRIPT_DIR, 'eat.wav')
if os.path.exists(filepath):
return filepath
sample_rate = 44100
duration = 0.15
n_samples = int(sample_rate * duration)
samples = []
for i in range(n_samples):
t = i / sample_rate
freq = 800 + (1200 - 800) * (t / duration)
envelope = math.sin(math.pi * t / duration)
sample = 0.4 * envelope * math.sin(2 * math.pi * freq * t)
samples.append(int(sample * 32767))
with wave.open(filepath, 'w') as wav_file:
wav_file.setnchannels(1)
wav_file.setsampwidth(2)
wav_file.setframerate(sample_rate)
for sample in samples:
wav_file.writeframes(struct.pack('<h', sample))
return filepath
def generate_game_over_sound():
filepath = os.path.join(SCRIPT_DIR, 'game_over.wav')
if os.path.exists(filepath):
return filepath
sample_rate = 44100
duration = 0.8
n_samples = int(sample_rate * duration)
samples = []
for i in range(n_samples):
t = i / sample_rate
freq = 400 - (200 * (t / duration))
envelope = max(0, 1 - (t / duration))
sample = 0.5 * envelope * math.sin(2 * math.pi * freq * t)
samples.append(int(sample * 32767))
with wave.open(filepath, 'w') as wav_file:
wav_file.setnchannels(1)
wav_file.setsampwidth(2)
wav_file.setframerate(sample_rate)
for sample in samples:
wav_file.writeframes(struct.pack('<h', sample))
return filepath
def generate_move_sound():
filepath = os.path.join(SCRIPT_DIR, 'move.wav')
if os.path.exists(filepath):
return filepath
sample_rate = 44100
duration = 0.05
n_samples = int(sample_rate * duration)
samples = []
for i in range(n_samples):
t = i / sample_rate
freq = 600
envelope = math.sin(math.pi * t / duration)
sample = 0.2 * envelope * math.sin(2 * math.pi * freq * t)
samples.append(int(sample * 32767))
with wave.open(filepath, 'w') as wav_file:
wav_file.setnchannels(1)
wav_file.setsampwidth(2)
wav_file.setframerate(sample_rate)
for sample in samples:
wav_file.writeframes(struct.pack('<h', sample))
return filepath
def ensure_sound_files():
sounds = ['eat.wav', 'game_over.wav', 'move.wav']
missing = [s for s in sounds if not os.path.exists(os.path.join(SCRIPT_DIR, s))]
if missing:
print("Generating sound files...")
generate_eat_sound()
generate_game_over_sound()
generate_move_sound()
print("Sound files generated!")
try:
import pygame
pygame.mixer.init()
SOUND_ENABLED = True
except ImportError:
SOUND_ENABLED = False
CELL_SIZE = 36
GRID_WIDTH = 10
GRID_HEIGHT = 16
WINDOW_WIDTH = CELL_SIZE * GRID_WIDTH
WINDOW_HEIGHT = CELL_SIZE * GRID_HEIGHT
BG_COLOR = "#000000"
SNAKE_HEAD_COLOR = "#33b233"
SNAKE_BODY_COLOR = "#2a8f2a"
SNAKE_TAIL_COLOR = "#1f6b1f"
FOOD_COLOR = "#ff4444"
TEXT_COLOR = "#ffffff"
GAME_OVER_COLOR = "#ff5555"
HEADER_BG = "#74b9e7"
INITIAL_SPEED = 180
MIN_SPEED = 40
SPEED_INCREMENT = 5
BOOST_MULTIPLIER = 8
FOOD_TIMER = 10
SCORE_FILE = "highscore.json"
class SnakeGame:
def __init__(self, root):
self.root = root
self.root.title("Snake Game")
self.root.resizable(False, False)
self.header_frame = tk.Frame(root, bg=HEADER_BG, height=60)
self.header_frame.pack(fill=tk.X)
self.header_frame.pack_propagate(False)
self.top_score_label = tk.Label(
self.header_frame,
text="Top: 0",
font=("Arial", 20, "bold"),
fg=TEXT_COLOR,
bg=HEADER_BG
)
self.top_score_label.pack(side=tk.LEFT, padx=20, pady=15)
self.boost_label = tk.Label(
self.header_frame,
text="",
font=("Arial", 20, "bold"),
fg="#ffcc00",
bg=HEADER_BG
)
self.boost_label.pack(side=tk.LEFT, expand=True, pady=15)
self.score_label = tk.Label(
self.header_frame,
text="Score: 0",
font=("Arial", 20, "bold"),
fg=TEXT_COLOR,
bg=HEADER_BG
)
self.score_label.pack(side=tk.RIGHT, padx=20, pady=15)
self.canvas = tk.Canvas(
root,
width=WINDOW_WIDTH,
height=WINDOW_HEIGHT,
bg=BG_COLOR,
highlightthickness=0
)
self.canvas.pack()
self.root.bind("<KeyPress>", self.handle_key_press)
self.root.bind("<KeyRelease>", self.handle_key_release)
self.key_pressed = None
self.key_press_time = 0
self.is_boosting = False
self.load_high_score()
self.load_sounds()
self.reset_game()
self.game_loop()
def load_high_score(self):
self.high_score = 0
try:
score_path = os.path.join(SCRIPT_DIR, SCORE_FILE)
if os.path.exists(score_path):
with open(score_path, 'r') as f:
data = json.load(f)
self.high_score = data.get('high_score', 0)
except:
pass
self.top_score_label.config(text=f"Top: {self.high_score}")
def save_high_score(self):
if self.score > self.high_score:
self.high_score = self.score
self.top_score_label.config(text=f"Top: {self.high_score}")
try:
score_path = os.path.join(SCRIPT_DIR, SCORE_FILE)
with open(score_path, 'w') as f:
json.dump({'high_score': self.high_score}, f)
except:
pass
def load_sounds(self):
self.sounds = {}
if SOUND_ENABLED:
sound_files = {
'eat': 'eat.wav',
'game_over': 'game_over.wav',
'move': 'move.wav'
}
for name, filename in sound_files.items():
path = os.path.join(SCRIPT_DIR, filename)
if os.path.exists(path):
try:
self.sounds[name] = pygame.mixer.Sound(path)
except:
pass
def play_sound(self, name):
if SOUND_ENABLED and name in self.sounds:
try:
self.sounds[name].play()
except:
pass
def reset_game(self):
center_x = GRID_WIDTH // 2
center_y = GRID_HEIGHT // 2
self.snake = [
(center_x, center_y),
(center_x - 1, center_y),
(center_x - 2, center_y)
]
self.direction = (1, 0)
self.next_direction = (1, 0)
self.score = 0
self.game_over = False
self.speed = INITIAL_SPEED
self.is_boosting = False
self.key_pressed = None
self.food = None
self.food_spawn_time = time.time()
self.spawn_food()
self.score_label.config(text=f"Score: {self.score}")
def spawn_food(self):
while True:
x = random.randint(0, GRID_WIDTH - 1)
y = random.randint(0, GRID_HEIGHT - 1)
if (x, y) not in self.snake:
self.food = (x, y)
self.food_spawn_time = time.time()
break
def handle_key_press(self, event):
key = event.keysym.lower()
if self.game_over:
if key == 'r':
self.reset_game()
return
dx, dy = self.direction
if key in ('up', 'w') and dy != 1:
self.next_direction = (0, -1)
self.key_pressed = key
self.key_press_time = time.time()
self.play_sound('move')
elif key in ('down', 's') and dy != -1:
self.next_direction = (0, 1)
self.key_pressed = key
self.key_press_time = time.time()
self.play_sound('move')
elif key in ('left', 'a') and dx != 1:
self.next_direction = (-1, 0)
self.key_pressed = key
self.key_press_time = time.time()
self.play_sound('move')
elif key in ('right', 'd') and dx != -1:
self.next_direction = (1, 0)
self.key_pressed = key
self.key_press_time = time.time()
self.play_sound('move')
def handle_key_release(self, event):
key = event.keysym.lower()
if key == self.key_pressed:
self.key_pressed = None
self.is_boosting = False
def check_boost(self):
if self.key_pressed and not self.game_over:
held_time = time.time() - self.key_press_time
if held_time > 0.3:
self.is_boosting = True
else:
self.is_boosting = False
else:
self.is_boosting = False
def check_food_timer(self):
if self.food:
elapsed = time.time() - self.food_spawn_time
if elapsed >= FOOD_TIMER:
self.spawn_food()
def wrap_position(self, x, y):
x = x % GRID_WIDTH
y = y % GRID_HEIGHT
return x, y
def move_snake(self):
self.direction = self.next_direction
head_x, head_y = self.snake[0]
dx, dy = self.direction
new_head = self.wrap_position(head_x + dx, head_y + dy)
if new_head in self.snake:
self.game_over = True
self.play_sound('game_over')
self.save_high_score()
return
self.snake.insert(0, new_head)
if new_head == self.food:
self.score += 10
self.score_label.config(text=f"Score: {self.score}")
self.spawn_food()
self.play_sound('eat')
if self.speed > MIN_SPEED:
self.speed -= SPEED_INCREMENT
else:
self.snake.pop()
def draw_snake_segment(self, x, y, index, total):
progress = index / max(total - 1, 1)
if index == 0:
color = SNAKE_HEAD_COLOR
size = CELL_SIZE - 2
elif index == total - 1:
color = SNAKE_TAIL_COLOR
size = int((CELL_SIZE - 4) * 0.6)
else:
scale = 1.0 - (progress * 0.3)
color = self.interpolate_color(SNAKE_BODY_COLOR, SNAKE_TAIL_COLOR, progress)
size = int((CELL_SIZE - 4) * scale)
px = x * CELL_SIZE + (CELL_SIZE - size) // 2
py = y * CELL_SIZE + (CELL_SIZE - size) // 2
self.canvas.create_oval(
px, py, px + size, py + size,
fill=color, outline=""
)
if index == 0:
self.draw_snake_eyes(x, y, size)
def draw_snake_eyes(self, x, y, size):
dx, dy = self.direction
cx = x * CELL_SIZE + CELL_SIZE // 2
cy = y * CELL_SIZE + CELL_SIZE // 2
eye_offset = 4
eye_size = 3
pupil_size = 2
if dx == 1:
eye_positions = [(cx + 2, cy - eye_offset), (cx + 2, cy + eye_offset)]
elif dx == -1:
eye_positions = [(cx - 2, cy - eye_offset), (cx - 2, cy + eye_offset)]
elif dy == -1:
eye_positions = [(cx - eye_offset, cy - 2), (cx + eye_offset, cy - 2)]
else:
eye_positions = [(cx - eye_offset, cy + 2), (cx + eye_offset, cy + 2)]
for ex, ey in eye_positions:
self.canvas.create_oval(
ex - eye_size, ey - eye_size,
ex + eye_size, ey + eye_size,
fill="white", outline=""
)
self.canvas.create_oval(
ex - pupil_size, ey - pupil_size,
ex + pupil_size, ey + pupil_size,
fill="black", outline=""
)
tongue_dx = dx * (CELL_SIZE // 2 + 3)
tongue_dy = dy * (CELL_SIZE // 2 + 3)
self.canvas.create_rectangle(
cx + tongue_dx - 1, cy + tongue_dy - 1,
cx + tongue_dx + 4, cy + tongue_dy + 1,
fill="#cc3344", outline=""
)
def interpolate_color(self, color1, color2, factor):
r1, g1, b1 = int(color1[1:3], 16), int(color1[3:5], 16), int(color1[5:7], 16)
r2, g2, b2 = int(color2[1:3], 16), int(color2[3:5], 16), int(color2[5:7], 16)
r = int(r1 + (r2 - r1) * factor)
g = int(g1 + (g2 - g1) * factor)
b = int(b1 + (b2 - b1) * factor)
return f"#{r:02x}{g:02x}{b:02x}"
def draw_food(self):
if self.food:
x, y = self.food
padding = 2
elapsed = time.time() - self.food_spawn_time
remaining = max(0, FOOD_TIMER - elapsed)
if remaining <= 3:
color = "#ff0000"
elif remaining <= 5:
color = "#ff8800"
else:
color = FOOD_COLOR
self.canvas.create_oval(
x * CELL_SIZE + padding,
y * CELL_SIZE + padding,
(x + 1) * CELL_SIZE - padding,
(y + 1) * CELL_SIZE - padding,
fill=color, outline=""
)
cx = x * CELL_SIZE + CELL_SIZE // 2
cy = y * CELL_SIZE + CELL_SIZE // 2
self.canvas.create_text(
cx, cy,
text=f"{int(remaining)}",
font=("Arial", 18, "bold"),
fill="white"
)
def draw_boost_indicator(self):
if self.is_boosting:
self.boost_label.config(text=" BOOST ")
else:
self.boost_label.config(text="")
def draw_game_over(self):
cx = WINDOW_WIDTH // 2
cy = WINDOW_HEIGHT // 2
self.canvas.create_rectangle(
cx - 120, cy - 80, cx + 120, cy + 80,
fill="#0d0d1a", outline="#333355", width=2
)
self.canvas.create_text(
cx, cy - 40,
text="GAME OVER",
font=("Arial", 28, "bold"),
fill=GAME_OVER_COLOR
)
self.canvas.create_text(
cx, cy + 5,
text=f"Score: {self.score}",
font=("Arial", 18),
fill=TEXT_COLOR
)
if self.score >= self.high_score:
self.canvas.create_text(
cx, cy + 30,
text="NEW HIGH SCORE!",
font=("Arial", 12, "bold"),
fill="#ffcc00"
)
self.canvas.create_text(
cx, cy + 55,
text="Press R to Restart",
font=("Arial", 14),
fill="#888888"
)
def draw(self):
self.canvas.delete("all")
for i, (x, y) in enumerate(self.snake):
self.draw_snake_segment(x, y, i, len(self.snake))
self.draw_food()
self.draw_boost_indicator()
if self.game_over:
self.draw_game_over()
def game_loop(self):
if not self.game_over:
self.check_boost()
self.check_food_timer()
self.move_snake()
self.draw()
current_speed = self.speed
if self.is_boosting and not self.game_over:
current_speed = max(MIN_SPEED, self.speed // BOOST_MULTIPLIER)
self.root.after(current_speed, self.game_loop)
def main():
ensure_sound_files()
root = tk.Tk()
root.configure(bg=BG_COLOR)
game = SnakeGame(root)
root.mainloop()
if __name__ == "__main__":
main()
Happy coding!