Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Alien Invasion in Python with Pygame: Refactored Source Code

Tech 1

alien_invasion.py

import pygame
from pygame.sprite import Group

from settings import Config
from game_stats import GameState
from scoreboard import HUD
from button import Button
from ship import Ship
import game_functions as gf


def main():
    pygame.init()

    cfg = Config()
    screen = pygame.display.set_mode((cfg.screen_w, cfg.screen_h))
    pygame.display.set_caption("Alien Invasion")

    start_button = Button(cfg, screen, "Play")

    state = GameState(cfg)
    hud = HUD(cfg, screen, state)

    ship = Ship(cfg, screen)
    shots = Group()
    swarm = Group()

    gf.build_fleet(cfg, screen, ship, swarm)

    while True:
        gf.process_input(cfg, screen, state, hud, start_button, ship, swarm, shots)

        if state.active:
            ship.update()
            gf.step_shots(cfg, screen, state, hud, ship, swarm, shots)
            gf.step_swarm(cfg, screen, state, hud, ship, swarm, shots)

        gf.draw_frame(cfg, screen, state, hud, ship, swarm, shots, start_button)


if __name__ == "__main__":
    main()

game_functions.py

import sys
from time import sleep

import pygame

from bullet import Laser
from alien import Invader


def handle_keydown(evt, cfg, screen, ship, shots):
    if evt.key == pygame.K_RIGHT:
        ship.heading_right = True
    elif evt.key == pygame.K_LEFT:
        ship.heading_left = True
    elif evt.key == pygame.K_SPACE:
        spawn_laser(cfg, screen, ship, shots)
    elif evt.key == pygame.K_q:
        sys.exit()


def handle_keyup(evt, ship):
    if evt.key == pygame.K_RIGHT:
        ship.heading_right = False
    elif evt.key == pygame.K_LEFT:
        ship.heading_left = False


def process_input(cfg, screen, state, hud, start_button, ship, swarm, shots):
    for evt in pygame.event.get():
        if evt.type == pygame.QUIT:
            sys.exit()
        elif evt.type == pygame.KEYDOWN:
            handle_keydown(evt, cfg, screen, ship, shots)
        elif evt.type == pygame.KEYUP:
            handle_keyup(evt, ship)
        elif evt.type == pygame.MOUSEBUTTONDOWN:
            mx, my = pygame.mouse.get_pos()
            start_game_if_clicked(cfg, screen, state, hud, start_button, ship, swarm, shots, mx, my)


def start_game_if_clicked(cfg, screen, state, hud, start_button, ship, swarm, shots, mx, my):
    clicked = start_button.rect.collidepoint(mx, my)
    if clicked and not state.active:
        cfg.init_dynamic()
        pygame.mouse.set_visible(False)

        state.reset()
        state.active = True

        hud.update_score_surface()
        hud.update_high_score_surface()
        hud.update_level_surface()
        hud.update_lives_group()

        swarm.empty()
        shots.empty()

        build_fleet(cfg, screen, ship, swarm)
        ship.recenter()


def draw_frame(cfg, screen, state, hud, ship, swarm, shots, start_button):
    screen.fill(cfg.background_color)

    for s in shots.sprites():
        s.draw()

    ship.blitme()
    swarm.draw(screen)

    hud.draw()

    if not state.active:
        start_button.draw_button()

    pygame.display.flip()


def step_shots(cfg, screen, state, hud, ship, swarm, shots):
    shots.update()

    for s in shots.copy():
        if s.rect.bottom <= 0:
            shots.remove(s)

    resolve_impacts(cfg, screen, state, hud, ship, swarm, shots)


def resolve_impacts(cfg, screen, state, hud, ship, swarm, shots):
    impacts = pygame.sprite.groupcollide(shots, swarm, True, True)

    if impacts:
        destroyed = sum(len(v) for v in impacts.values())
        state.score += cfg.invader_points * destroyed
        hud.update_score_surface()
        update_high_score(state, hud)

    if len(swarm) == 0:
        shots.empty()
        cfg.accelerate()
        state.level += 1
        hud.update_level_surface()
        build_fleet(cfg, screen, ship, swarm)


def spawn_laser(cfg, screen, ship, shots):
    if len(shots) < cfg.shot_limit:
        shots.add(Laser(cfg, screen, ship))


def columns_for_row(cfg, invader_w):
    free_x = cfg.screen_w - 2 * invader_w
    return int(free_x // (2 * invader_w))


def rows_for_screen(cfg, ship_h, invader_h):
    free_y = cfg.screen_h - (3 * invader_h) - ship_h
    return int(free_y // (2 * invader_h))


def add_invader(cfg, screen, swarm, col_idx, row_idx):
    inv = Invader(cfg, screen)
    w = inv.rect.width
    inv.x = w + 2 * w * col_idx
    inv.rect.x = inv.x
    inv.rect.y = inv.rect.height + 2 * inv.rect.height * row_idx
    swarm.add(inv)


def build_fleet(cfg, screen, ship, swarm):
    sample = Invader(cfg, screen)
    cols = columns_for_row(cfg, sample.rect.width)
    rows = rows_for_screen(cfg, ship.rect.height, sample.rect.height)

    for r in range(rows):
        for c in range(cols):
            add_invader(cfg, screen, swarm, c, r)


def on_edge(cfg, swarm):
    for inv in swarm.sprites():
        if inv.check_edges():
            shift_and_flip(cfg, swarm)
            break


def shift_and_flip(cfg, swarm):
    for inv in swarm.sprites():
        inv.rect.y += cfg.fleet_drop
    cfg.fleet_dir *= -1


def ship_hit(cfg, screen, state, hud, ship, swarm, shots):
    if state.lives > 0:
        state.lives -= 1
        hud.update_lives_group()

        swarm.empty()
        shots.empty()

        build_fleet(cfg, screen, ship, swarm)
        ship.recenter()

        sleep(0.5)
    else:
        state.active = False
        pygame.mouse.set_visible(True)


def step_swarm(cfg, screen, state, hud, ship, swarm, shots):
    on_edge(cfg, swarm)
    swarm.update()

    if pygame.sprite.spritecollideany(ship, swarm):
        ship_hit(cfg, screen, state, hud, ship, swarm, shots)

    check_bottom(cfg, screen, state, hud, ship, swarm, shots)


def check_bottom(cfg, screen, state, hud, ship, swarm, shots):
    screen_rect = screen.get_rect()
    for inv in swarm.sprites():
        if inv.rect.bottom >= screen_rect.bottom:
            ship_hit(cfg, screen, state, hud, ship, swarm, shots)
            break


def update_high_score(state, hud):
    if state.score > state.high_score:
        state.high_score = state.score
        hud.update_high_score_surface()

alien.py

import pygame
from pygame.sprite import Sprite


class Invader(Sprite):
    def __init__(self, cfg, screen):
        super().__init__()
        self.screen = screen
        self.cfg = cfg

        self.image = pygame.image.load("images/alien.bmp")
        self.rect = self.image.get_rect()

        self.rect.x = self.rect.width
        self.rect.y = self.rect.height

        self.x = float(self.rect.x)

    def blitme(self):
        self.screen.blit(self.image, self.rect)

    def check_edges(self):
        bounds = self.screen.get_rect()
        return self.rect.right >= bounds.right or self.rect.left <= 0

    def update(self):
        self.x += self.cfg.invader_speed * self.cfg.fleet_dir
        self.rect.x = self.x

bulllet.py

import pygame
from pygame.sprite import Sprite


class Laser(Sprite):
    def __init__(self, cfg, screen, ship):
        super().__init__()
        self.screen = screen

        self.rect = pygame.Rect(0, 0, cfg.shot_w, cfg.shot_h)
        self.rect.centerx = ship.rect.centerx
        self.rect.top = ship.rect.top

        self.y = float(self.rect.y)
        self.color = cfg.shot_color
        self.speed = cfg.shot_speed

    def update(self):
        self.y -= self.speed
        self.rect.y = self.y

    def draw(self):
        pygame.draw.rect(self.screen, self.color, self.rect)

buttton.py

import pygame
import pygame.font


class Button:
    def __init__(self, cfg, screen, msg):
        self.screen = screen
        self.screen_rect = screen.get_rect()

        self.width, self.height = 200, 50
        self.bg_color = (1, 220, 212)
        self.text_color = (255, 255, 255)
        self.font = pygame.font.SysFont(None, 48)

        self.rect = pygame.Rect(0, 0, self.width, self.height)
        self.rect.center = self.screen_rect.center

        self._render_text(msg)

    def _render_text(self, msg):
        self.msg_image = self.font.render(msg, True, self.text_color, self.bg_color)
        self.msg_rect = self.msg_image.get_rect()
        self.msg_rect.center = self.rect.center

    def draw_button(self):
        self.screen.fill(self.bg_color, self.rect)
        self.screen.blit(self.msg_image, self.msg_rect)

game_stats.py

class GameState:
    def __init__(self, cfg):
        self.cfg = cfg
        self.active = False
        self.high_score = 0
        self.reset()

    def reset(self):
        self.lives = self.cfg.ship_lives
        self.score = 0
        self.level = 1

scoreboard.py

import pygame.font
from pygame.sprite import Group

from ship import Ship


class HUD:
    def __init__(self, cfg, screen, state):
        self.cfg = cfg
        self.screen = screen
        self.screen_rect = screen.get_rect()
        self.state = state

        self.text_color = (30, 30, 30)
        self.font = pygame.font.SysFont(None, 48)

        self.update_score_surface()
        self.update_high_score_surface()
        self.update_level_surface()
        self.update_lives_group()

    def update_score_surface(self):
        s = f"{self.state.score:,}"
        self.score_img = self.font.render(s, True, self.text_color, self.cfg.background_color)
        self.score_rect = self.score_img.get_rect()
        self.score_rect.right = self.screen_rect.right - 20
        self.score_rect.top = 20

    def update_high_score_surface(self):
        s = f"{self.state.high_score:,}"
        self.high_img = self.font.render(s, True, self.text_color, self.cfg.background_color)
        self.high_rect = self.high_img.get_rect()
        self.high_rect.centerx = self.screen_rect.centerx
        self.high_rect.top = self.score_rect.top

    def update_level_surface(self):
        s = str(self.state.level)
        self.level_img = self.font.render(s, True, self.text_color, self.cfg.background_color)
        self.level_rect = self.level_img.get_rect()
        self.level_rect.right = self.score_rect.right
        self.level_rect.top = self.score_rect.bottom + 10

    def update_lives_group(self):
        self.lives_group = Group()
        for i in range(self.state.lives):
            ship = Ship(self.cfg, self.screen)
            ship.rect.x = 10 + i * ship.rect.width
            ship.rect.y = 10
            self.lives_group.add(ship)

    def draw(self):
        self.screen.blit(self.score_img, self.score_rect)
        self.screen.blit(self.high_img, self.high_rect)
        self.screen.blit(self.level_img, self.level_rect)
        self.lives_group.draw(self.screen)

settings.py

class Config:
    def __init__(self):
        # Display
        self.screen_w = 1200
        self.screen_h = 800
        self.background_color = (164, 177, 161)

        # Ship
        self.ship_lives = 3

        # Shots
        self.shot_w = 3
        self.shot_h = 15
        self.shot_color = (60, 60, 60)
        self.shot_limit = 3

        # Fleet
        self.fleet_drop = 10

        # Scaling
        self.speed_scale = 1.1
        self.points_scale = 1.5

        self.init_dynamic()

    def init_dynamic(self):
        self.ship_speed = 1.5
        self.shot_speed = 7.0
        self.invader_speed = 0.6
        self.fleet_dir = 1  # 1: right, -1: left
        self.invader_points = 50

    def accelerate(self):
        self.ship_speed *= self.speed_scale
        self.shot_speed *= self.speed_scale
        self.invader_speed *= self.speed_scale
        self.invader_points = int(self.invader_points * self.points_scale)

ship.py

import pygame
from pygame.sprite import Sprite


class Ship(Sprite):
    def __init__(self, cfg, screen):
        super().__init__()
        self.screen = screen
        self.cfg = cfg

        self.image = pygame.image.load("images/ship.bmp")
        self.rect = self.image.get_rect()
        self.screen_rect = screen.get_rect()

        self.rect.centerx = self.screen_rect.centerx
        self.rect.bottom = self.screen_rect.bottom

        self.center_x = float(self.rect.centerx)

        self.heading_right = False
        self.heading_left = False

    def update(self):
        if self.heading_right and self.rect.right < self.screen_rect.right:
            self.center_x += self.cfg.ship_speed
        if self.heading_left and self.rect.left > 0:
            self.center_x -= self.cfg.ship_speed
        self.rect.centerx = self.center_x

    def blitme(self):
        self.screen.blit(self.image, self.rect)

    def recenter(self):
        self.center_x = self.screen_rect.centerx

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.