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:

  1. Creates a sine wave at the specified frequency
  2. Applies an envelope (fade in/out) using sin()
  3. Converts to 16-bit PCM audio format
  4. Writes to a WAV file

Sound Effects

The game generates three distinct sounds:

SoundFrequencyDurationPurpose
Eat800-1200Hz sweep0.15sFood collection
Move600Hz0.05sDirection change
Game Over400-200Hz drop0.8sCollision

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:

  1. Player holds a movement key
  2. After 0.3 seconds, boost activates
  3. Snake moves 8x faster during boost
  4. 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

  1. Procedural audio eliminates external file dependencies
  2. Gradient rendering creates polished visuals
  3. Boost mechanics add depth to gameplay
  4. Directional eyes make the game feel responsive
  5. Food timers create urgency and prevent camping
  6. 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!