🚀 Python Lesson 11: Gravitational Destroyer Game

Welcome to the ultimate fusion of physics and gaming! In this lesson, we'll create "Gravitational Destroyer" - an epic space shooting game that uses real N-Body gravitational physics. This combines everything we've learned about physics simulation with thrilling game mechanics.

🎯 What You'll Learn

  • Advanced game development with Python and Tkinter
  • Real-time physics simulation in games
  • Object-oriented programming for game entities
  • Collision detection and response systems
  • Game state management and progression
  • Strategic gameplay design using physics

🎮 Game Concept

Gravitational Destroyer is an Asteroids-style shooting game where you pilot a spaceship through gravitational fields created by planets. Your mission: destroy all planets while avoiding collisions and managing your fuel supply. The twist? Everything follows real gravitational physics!

🌟 Game Features

  • Realistic Physics: All objects affected by gravitational forces
  • Progressive Levels: 3, 5, 10, 15, 20+ planets as you advance
  • Special Planets: Fuel depots and explosive chain-reaction planets
  • Strategic Gameplay: Use gravity to curve shots around obstacles
  • Fuel Management: Limited thrust requires careful planning

⚗️ The Physics Behind the Game

Our game implements several key physics concepts:

1. Gravitational Force

Every planet exerts gravitational force on the spaceship and bullets:

F = G × (m₁ × m₂) / r²

Where G is the gravitational constant, m₁ and m₂ are masses, and r is distance.

2. Orbital Mechanics

Planets follow realistic orbital paths, creating dynamic gravitational fields that change over time.

3. Projectile Motion in Gravity Fields

Bullets curve around planets, allowing for strategic "slingshot" shots and gravity-assisted targeting.

💻 The Complete Game Code

Here's the full implementation of Gravitational Destroyer:

import math
import tkinter as tk
from tkinter import Canvas, Label, Frame
import time
import random

class GameObject:
    """Base class for all game objects"""
    def __init__(self, x, y, vx=0, vy=0, radius=5, color="white"):
        self.x = x
        self.y = y
        self.vx = vx
        self.vy = vy
        self.radius = radius
        self.color = color
        self.alive = True
    
    def update(self, dt):
        """Update position based on velocity"""
        self.x += self.vx * dt
        self.y += self.vy * dt
    
    def draw(self, canvas):
        """Draw the object"""
        canvas.create_oval(
            self.x - self.radius, self.y - self.radius,
            self.x + self.radius, self.y + self.radius,
            fill=self.color, outline="white"
        )

class Planet(GameObject):
    """A planet with gravitational effects"""
    def __init__(self, x, y, vx, vy, mass, color, planet_type="normal"):
        radius = max(8, math.sqrt(mass) * 1.2)
        super().__init__(x, y, vx, vy, radius, color)
        self.mass = mass
        self.planet_type = planet_type
        self.trail = []
        self.points = int(mass / 10)  # Points based on mass
    
    def update(self, dt):
        super().update(dt)
        
        # Screen wrapping - planets reappear on opposite side
        if self.x < -50:
            self.x = 1050
        elif self.x > 1050:
            self.x = -50
        if self.y < -50:
            self.y = 690
        elif self.y > 690:
            self.y = -50
        
        # Add to trail for visual effect
        self.trail.append((self.x, self.y))
        if len(self.trail) > 30:
            self.trail.pop(0)
    
    def draw(self, canvas):
        # Draw orbital trail
        if len(self.trail) > 1:
            for i in range(1, len(self.trail)):
                fade = i / len(self.trail)
                alpha = int(255 * fade * 0.3)
                x1, y1 = self.trail[i-1]
                x2, y2 = self.trail[i]
                canvas.create_line(x1, y1, x2, y2, 
                                 fill="#" + format(alpha, '02x') * 3, width=1)
        
        # Draw planet with special effects
        if self.planet_type == "fuel":
            # Pulsing green fuel depot
            pulse = 1 + 0.3 * math.sin(time.time() * 5)
            canvas.create_oval(
                self.x - self.radius * pulse, self.y - self.radius * pulse,
                self.x + self.radius * pulse, self.y + self.radius * pulse,
                fill=self.color, outline="lime", width=2
            )
        elif self.planet_type == "explosive":
            # Red explosive planet
            canvas.create_oval(
                self.x - self.radius, self.y - self.radius,
                self.x + self.radius, self.y + self.radius,
                fill=self.color, outline="red", width=2
            )
        else:
            # Normal planet
            super().draw(canvas)

class Spaceship(GameObject):
    """Player-controlled spaceship"""
    def __init__(self, x, y):
        super().__init__(x, y, 0, 0, 8, "cyan")
        self.angle = 0  # Ship orientation
        self.thrust_power = 200
        self.fuel = 100
        self.max_fuel = 100
        self.shield = False
        self.shield_time = 0
        self.invulnerable_time = 0
    
    def apply_thrust(self, dt):
        """Apply thrust in the direction the ship is facing"""
        if self.fuel > 0:
            thrust_x = math.cos(self.angle) * self.thrust_power * dt
            thrust_y = math.sin(self.angle) * self.thrust_power * dt
            self.vx += thrust_x
            self.vy += thrust_y
            self.fuel -= 20 * dt
            self.fuel = max(0, self.fuel)
            return True
        return False
    
    def rotate(self, direction, dt):
        """Rotate the ship"""
        self.angle += direction * 3 * dt
    
    def update(self, dt):
        """Update spaceship with screen wrapping"""
        super().update(dt)
        
        # Screen wrapping - spaceship reappears on opposite side
        if self.x < -50:
            self.x = 1050
        elif self.x > 1050:
            self.x = -50
        if self.y < -50:
            self.y = 690
        elif self.y > 690:
            self.y = -50
    
    def draw(self, canvas):
        # Draw ship as a triangle pointing in movement direction
        size = self.radius
        tip_x = self.x + size * math.cos(self.angle)
        tip_y = self.y + size * math.sin(self.angle)
        
        left_x = self.x + size * 0.6 * math.cos(self.angle + 2.5)
        left_y = self.y + size * 0.6 * math.sin(self.angle + 2.5)
        
        right_x = self.x + size * 0.6 * math.cos(self.angle - 2.5)
        right_y = self.y + size * 0.6 * math.sin(self.angle - 2.5)
        
        # Draw shield if active
        if self.shield:
            canvas.create_oval(
                self.x - size * 1.5, self.y - size * 1.5,
                self.x + size * 1.5, self.y + size * 1.5,
                outline="blue", width=2
            )
        
        # Draw ship (flashing if invulnerable)
        if self.invulnerable_time <= 0 or int(time.time() * 10) % 2:
            canvas.create_polygon(
                tip_x, tip_y, left_x, left_y, right_x, right_y,
                fill=self.color, outline="white"
            )

class GravitationalDestroyer:
    """Main game class implementing the complete game loop"""
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("Gravitational Destroyer")
        self.root.geometry("1000x700")
        self.root.configure(bg='black')
        
        # Game state
        self.level = 1
        self.score = 0
        self.lives = 3
        self.game_over = False
        
        # Physics constants
        self.G = 300  # Gravitational constant
        
        # Game objects
        self.spaceship = None
        self.planets = []
        self.bullets = []
        
        self.setup_ui()
        self.start_level()
        self.bind_controls()
    
    def apply_gravity(self, dt):
        """Apply gravitational forces to all objects"""
        # Standard mode: only spaceship and bullets affected by planets
        all_objects = [self.spaceship] + self.bullets
        
        for obj in all_objects:
            if not obj or not obj.alive:
                continue
                
            # Calculate gravitational force from each planet
            ax = ay = 0
            for planet in self.planets:
                if not planet.alive:
                    continue
                    
                dx = planet.x - obj.x
                dy = planet.y - obj.y
                distance_sq = dx*dx + dy*dy
                distance = math.sqrt(distance_sq)
                
                if distance > planet.radius + obj.radius:
                    # Newton's Law of Universal Gravitation
                    force = self.G * planet.mass / distance_sq
                    ax += force * dx / distance
                    ay += force * dy / distance
            
            # Apply acceleration to velocity
            obj.vx += ax * dt
            obj.vy += ay * dt
        
        # Realistic mode: planets also attract each other (N-body physics)
        if self.realistic_mode:
            for i, planet1 in enumerate(self.planets):
                if not planet1.alive:
                    continue
                    
                ax = ay = 0
                for j, planet2 in enumerate(self.planets):
                    if i == j or not planet2.alive:
                        continue
                        
                    dx = planet2.x - planet1.x
                    dy = planet2.y - planet1.y
                    distance_sq = dx*dx + dy*dy
                    distance = math.sqrt(distance_sq)
                    
                    if distance > planet1.radius + planet2.radius:
                        # Gravitational force between planets
                        force = self.G * planet2.mass / distance_sq
                        ax += force * dx / distance
                        ay += force * dy / distance
                
                # Apply acceleration to planet velocity
                planet1.vx += ax * dt
                planet1.vy += ay * dt
    
    def game_loop(self):
        """Main game loop running at ~60 FPS"""
        current_time = time.time()
        if hasattr(self, 'last_time'):
            dt = current_time - self.last_time
        else:
            dt = 0.016
        
        self.last_time = current_time
        
        # Update game state
        self.handle_input(dt)
        self.apply_gravity(dt)
        self.update_objects(dt)
        self.check_collisions()
        self.draw_game()
        
        # Schedule next frame
        self.root.after(16, self.game_loop)
    
    def setup_ui(self):
        """Create the game UI elements"""
        # Create main frame
        self.main_frame = Frame(self.root, bg='black')
        self.main_frame.pack(fill=tk.BOTH, expand=True)
        
        # Create info panel
        self.info_frame = Frame(self.main_frame, bg='black', height=60)
        self.info_frame.pack(fill=tk.X, side=tk.TOP)
        self.info_frame.pack_propagate(False)
        
        # Game info labels
        self.score_label = Label(self.info_frame, text="Score: 0", 
                                fg='white', bg='black', font=('Arial', 14))
        self.score_label.pack(side=tk.LEFT, padx=10, pady=10)
        
        self.level_label = Label(self.info_frame, text="Level: 1", 
                                fg='white', bg='black', font=('Arial', 14))
        self.level_label.pack(side=tk.LEFT, padx=10, pady=10)
        
        self.lives_label = Label(self.info_frame, text="Lives: 3", 
                                fg='white', bg='black', font=('Arial', 14))
        self.lives_label.pack(side=tk.LEFT, padx=10, pady=10)
        
        self.fuel_label = Label(self.info_frame, text="Fuel: 100%", 
                               fg='white', bg='black', font=('Arial', 14))
        self.fuel_label.pack(side=tk.LEFT, padx=10, pady=10)
        
        # Create game canvas
        self.canvas = Canvas(self.main_frame, bg='black', width=1000, height=640)
        self.canvas.pack(fill=tk.BOTH, expand=True)
        
        # Game status
        self.paused = False
        self.keys_pressed = set()
        self.realistic_mode = False  # Toggle for N-body physics
    
    def start_level(self):
        """Initialize a new level"""
        # Create spaceship at center
        self.spaceship = Spaceship(500, 320)
        
        # Clear existing objects
        self.planets = []
        self.bullets = []
        
        # Generate planets for this level
        num_planets = min(3 + self.level * 2, 20)
        
        for i in range(num_planets):
            # Random position (not too close to center)
            angle = random.uniform(0, 2 * math.pi)
            distance = random.uniform(150, 300)
            x = 500 + distance * math.cos(angle)
            y = 320 + distance * math.sin(angle)
            
            # Random orbital velocity
            orbital_speed = random.uniform(20, 60)
            vx = -orbital_speed * math.sin(angle)
            vy = orbital_speed * math.cos(angle)
            
            # Random mass and color
            mass = random.uniform(50, 200)
            colors = ['orange', 'yellow', 'red', 'purple', 'blue']
            color = random.choice(colors)
            
            # Special planet types
            planet_type = "normal"
            if random.random() < 0.1:  # 10% chance for fuel depot
                planet_type = "fuel"
                color = "green"
            elif random.random() < 0.05:  # 5% chance for explosive
                planet_type = "explosive"
                color = "red"
                mass *= 1.5
            
            planet = Planet(x, y, vx, vy, mass, color, planet_type)
            self.planets.append(planet)
    
    def bind_controls(self):
        """Set up keyboard controls"""
        self.root.focus_set()
        self.root.bind('<KeyPress>', self.key_press)
        self.root.bind('<KeyRelease>', self.key_release)
    
    def key_press(self, event):
        """Handle key press events"""
        self.keys_pressed.add(event.keysym.lower())
        
        if event.keysym.lower() == 'p':
            self.paused = not self.paused
        elif event.keysym.lower() == 'r':
            if self.game_over:
                self.restart_game()
            else:
                self.start_level()
        elif event.keysym.lower() == 'n':
            self.realistic_mode = not self.realistic_mode
    
    def key_release(self, event):
        """Handle key release events"""
        self.keys_pressed.discard(event.keysym.lower())
    
    def handle_input(self, dt):
        """Process continuous input"""
        if self.paused or self.game_over or not self.spaceship:
            return
        
        # Rotation
        if 'left' in self.keys_pressed:
            self.spaceship.rotate(-1, dt)
        if 'right' in self.keys_pressed:
            self.spaceship.rotate(1, dt)
        
        # Thrust
        if 'up' in self.keys_pressed:
            self.spaceship.apply_thrust(dt)
        
        # Shooting
        if 'space' in self.keys_pressed:
            self.shoot_bullet()
    
    def shoot_bullet(self):
        """Create a new bullet"""
        if not self.spaceship or len(self.bullets) >= 5:
            return
        
        # Create bullet at ship position with ship velocity + bullet speed
        bullet_speed = 300
        bullet_vx = self.spaceship.vx + bullet_speed * math.cos(self.spaceship.angle)
        bullet_vy = self.spaceship.vy + bullet_speed * math.sin(self.spaceship.angle)
        
        bullet = GameObject(self.spaceship.x, self.spaceship.y, bullet_vx, bullet_vy, 3, "yellow")
        self.bullets.append(bullet)
    
    def update_objects(self, dt):
        """Update all game objects"""
        if self.paused:
            return
        
        # Update spaceship
        if self.spaceship:
            self.spaceship.update(dt)
            if self.spaceship.invulnerable_time > 0:
                self.spaceship.invulnerable_time -= dt
        
        # Update planets
        for planet in self.planets[:]:
            planet.update(dt)
        
        # Update bullets
        for bullet in self.bullets[:]:
            bullet.update(dt)
            # Remove bullets that go off screen
            if (bullet.x < -50 or bullet.x > 1050 or 
                bullet.y < -50 or bullet.y > 690):
                self.bullets.remove(bullet)
    
    def check_collisions(self):
        """Check for collisions between objects"""
        if not self.spaceship or self.spaceship.invulnerable_time > 0:
            return
        
        # Check spaceship-planet collisions
        for planet in self.planets[:]:
            if not planet.alive:
                continue
            dx = self.spaceship.x - planet.x
            dy = self.spaceship.y - planet.y
            distance = math.sqrt(dx*dx + dy*dy)
            
            if distance < self.spaceship.radius + planet.radius:
                if planet.planet_type == "fuel":
                    # Fuel depot - refuel ship
                    self.spaceship.fuel = min(100, self.spaceship.fuel + 50)
                    planet.alive = False
                    self.planets.remove(planet)
                    self.score += 50
                else:
                    # Collision with planet - lose life
                    self.lives -= 1
                    self.spaceship.invulnerable_time = 2.0
                    if self.lives <= 0:
                        self.game_over = True
        
        # Check bullet-planet collisions
        for bullet in self.bullets[:]:
            for planet in self.planets[:]:
                if not planet.alive:
                    continue
                dx = bullet.x - planet.x
                dy = bullet.y - planet.y
                distance = math.sqrt(dx*dx + dy*dy)
                
                if distance < bullet.radius + planet.radius:
                    # Hit!
                    self.score += planet.points
                    self.bullets.remove(bullet)
                    
                    if planet.planet_type == "explosive":
                        # Chain reaction - destroy nearby planets
                        self.explode_planet(planet)
                    else:
                        planet.alive = False
                        self.planets.remove(planet)
                    break
        
        # Check if level complete
        if len(self.planets) == 0:
            self.level += 1
            self.start_level()
    
    def explode_planet(self, explosive_planet):
        """Handle explosive planet chain reaction"""
        explosion_radius = explosive_planet.radius * 3
        planets_to_remove = [explosive_planet]
        
        for planet in self.planets:
            if planet == explosive_planet:
                continue
            dx = planet.x - explosive_planet.x
            dy = planet.y - explosive_planet.y
            distance = math.sqrt(dx*dx + dy*dy)
            
            if distance < explosion_radius:
                planets_to_remove.append(planet)
                self.score += planet.points * 2  # Bonus for chain reaction
        
        for planet in planets_to_remove:
            if planet in self.planets:
                self.planets.remove(planet)
    
    def draw_game(self):
        """Draw all game objects"""
        self.canvas.delete("all")
        
        if self.paused:
            self.canvas.create_text(500, 320, text="PAUSED", 
                                  fill="white", font=('Arial', 48))
            return
        
        if self.game_over:
            self.canvas.create_text(500, 280, text="GAME OVER", 
                                  fill="red", font=('Arial', 48))
            self.canvas.create_text(500, 340, text=f"Final Score: {self.score}", 
                                  fill="white", font=('Arial', 24))
            self.canvas.create_text(500, 380, text="Press R to restart", 
                                  fill="white", font=('Arial', 18))
            return
        
        # Draw planets
        for planet in self.planets:
            if planet.alive:
                planet.draw(self.canvas)
        
        # Draw spaceship
        if self.spaceship:
            self.spaceship.draw(self.canvas)
        
        # Draw bullets
        for bullet in self.bullets:
            bullet.draw(self.canvas)
        
        # Update UI labels
        self.score_label.config(text=f"Score: {self.score}")
        self.level_label.config(text=f"Level: {self.level}")
        self.lives_label.config(text=f"Lives: {self.lives}")
        if self.spaceship:
            fuel_percent = int(self.spaceship.fuel)
            self.fuel_label.config(text=f"Fuel: {fuel_percent}%")
        
        # Show physics mode indicator
        mode_text = "N-BODY PHYSICS" if self.realistic_mode else "GAME MODE"
        mode_color = "yellow" if self.realistic_mode else "cyan"
        self.canvas.create_text(500, 30, text=mode_text, 
                              fill=mode_color, font=('Arial', 12, 'bold'))
    
    def restart_game(self):
        """Restart the entire game"""
        self.level = 1
        self.score = 0
        self.lives = 3
        self.game_over = False
        self.start_level()
    
    def run(self):
        """Start the game"""
        self.game_loop()
        self.root.mainloop()

# Run the game
if __name__ == "__main__":
    game = GravitationalDestroyer()
    game.run()

📋 Copy this code into Spyder:

  1. 1. Open Spyder from Anaconda Navigator
  2. 2. Copy the code above into the editor
  3. 3. Save as gravitational_destroyer.py
  4. 4. Press F5 to run the game
  5. 5. Use arrow keys to move, SPACE to shoot!

🕹️ Gameplay Mechanics

Controls

  • Arrow Keys: Rotate ship and apply thrust
  • SPACE: Fire bullets
  • P: Pause/unpause game
  • R: Restart level (or game if game over)
  • N: Toggle N-Body Physics Mode (planets attract each other)

🎯 Strategic Elements

  • Fuel Management: Thrust consumes fuel - use gravity to conserve it
  • Gravity Assists: Use planet gravity to curve bullets around obstacles
  • Orbital Mechanics: Planets move in realistic orbits, creating dynamic challenges
  • Chain Reactions: Explosive planets can trigger cascading destructions

⚗️ Physics Modes

🎮 Game Mode (Default)

  • Planets affect spaceship and bullets only
  • Predictable, stable planetary orbits
  • Balanced gameplay experience
  • Easier to complete levels

🌌 N-Body Physics Mode (Press N)

  • Planets gravitationally attract each other
  • True N-body orbital dynamics
  • Chaotic, evolving planetary systems
  • Scientifically accurate but challenging

🧠 Educational Value

This game teaches advanced programming and physics concepts:

Programming Concepts

  • Object-oriented design patterns
  • Game loop architecture
  • Event-driven programming
  • Real-time graphics and animation
  • State management systems

Physics Concepts

  • Newton's Law of Universal Gravitation
  • Orbital mechanics and trajectories
  • Conservation of momentum and energy
  • Vector mathematics in 2D space
  • Collision detection algorithms

🚀 Possible Extensions

Challenge yourself by adding these features:

  • Power-ups: Shield generators, weapon upgrades, speed boosts
  • Multiple Ship Types: Different ships with unique characteristics
  • Boss Battles: Massive planets with special abilities
  • Multiplayer Mode: Two-player cooperative or competitive gameplay
  • Level Editor: Create and share custom gravitational challenges
  • Particle Effects: Explosions, thruster flames, and debris

🌐 Web Version

Experience Gravitational Destroyer directly in your browser! The web version includes all the same physics and gameplay mechanics, optimized for smooth browser performance.

🎉 Conclusion

Congratulations! You've created a sophisticated physics-based game that combines entertainment with education. Gravitational Destroyer demonstrates how real physics can create engaging and strategic gameplay mechanics. The game serves as both a fun experience and a practical demonstration of gravitational physics in action.

This project showcases advanced Python programming techniques, game development principles, and real-world physics simulation - skills that are valuable in both game development and scientific computing fields.