2
\$\begingroup\$

This is my first big project that I started after completing the Pygame Shmup guide put together by KidsCanCode. I'm starting to feel like it's very close to being finished so wanted to post my code here to find out what could be improved or what isn't up to standard. Brownie points for whoever can figure out why the alien animation bugs after reinforcements are generated!

Any comments and/or feedback would be greatly appreciated :)

Github link: https://github.com/Dreadnought88111/Space-WIP

# Space WIP
# By Elias

# things to implement
    # boss introduction cinematic
    # announ. boss sound
    # victory sound

# have aliens jump down vertically rather than diagonally

# known issues:
    # aliens speed up when reinforcements are being dropped
    # boss and bossvessel rect can be coliided with in level 1 despite them not being generated yet
        # temporary fix introduced in the check collisions function not checking collision until level > 9

import math
import pygame
import random
import sys
from os import path

pygame.init()
pygame.mixer.init()

# asset folders
FONT_DIR = path.join(path.dirname(__file__), "fonts")
IMAGE_DIR = path.join(path.dirname(__file__), "images")
SOUND_DIR = path.join(path.dirname(__file__), "sounds")

# screen
WIDTH = 800
HEIGHT = 600
SCREEN = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption('Space WIP')
pygame.display.set_icon(pygame.image.load(path.join(IMAGE_DIR, "logo.png")).convert_alpha())
FPS = 60

# colours
BLACK = (0, 0, 0)
BLUE = (0, 255, 240)
GREEN = (0, 255, 0)
ORANGE = (255, 173, 0)
PURPLE = (255, 0, 255)
RED = (255, 0, 0)
WHITE = (255, 255, 255)

# fonts
FONT = path.join(FONT_DIR, '8-BitMadness.ttf')
LOGOFONT = path.join(FONT_DIR, "edunline.ttf")
STARTFONT = path.join(FONT_DIR, "upheavtt.ttf")

# images
alien2_1 = pygame.image.load(path.join(IMAGE_DIR, "enemy2_1.png")).convert_alpha()
alien2_2 = pygame.image.load(path.join(IMAGE_DIR, "enemy2_2.png")).convert_alpha()
alien_images = [pygame.transform.scale(alien2_1, (40, 35)), pygame.transform.scale(alien2_2, (40, 35))]

alien3_1 = pygame.image.load(path.join(IMAGE_DIR, "enemy3_1.png")).convert_alpha()
alien3_2 = pygame.image.load(path.join(IMAGE_DIR, "enemy3_2.png")).convert_alpha()
alien_backup_images = [pygame.transform.scale(alien3_1, (40, 35)), pygame.transform.scale(alien3_2, (40, 35))]

alien1_1 = pygame.image.load(path.join(IMAGE_DIR, "enemy1_1.png")).convert_alpha()
alien1_2 = pygame.image.load(path.join(IMAGE_DIR, "enemy1_2.png")).convert_alpha()
alien_elite_images = [pygame.transform.scale(alien1_1, (40, 35)), pygame.transform.scale(alien1_2, (40, 35))]

# music
MUSICVOLUME = 0.5

BACKGROUND1 = path.join(SOUND_DIR, 'Background1.ogg')
BACKGROUND2 = path.join(SOUND_DIR, 'Background2.ogg')

# sounds
SOUNDVOLUME = 0.2

ALIENAPPEAR = pygame.mixer.Sound(path.join(SOUND_DIR, 'AlienAppear.wav'))
ALIENAPPEAR.set_volume(SOUNDVOLUME)

ALIENEXPLOSIONSOUND = pygame.mixer.Sound(path.join(SOUND_DIR, 'AlienExplosion.wav'))
ALIENEXPLOSIONSOUND.set_volume(SOUNDVOLUME)

ALIENMOVESOUND = pygame.mixer.Sound(path.join(SOUND_DIR, 'AlienMove.wav'))
ALIENMOVESOUND.set_volume(SOUNDVOLUME)

GAMEOVER = pygame.mixer.Sound(path.join(SOUND_DIR, 'GameOver.wav'))
GAMEOVER.set_volume(MUSICVOLUME)  # not an error

LASERSOUND = pygame.mixer.Sound(path.join(SOUND_DIR, 'Laser.wav'))
LASERSOUND.set_volume(SOUNDVOLUME)

MYSTERYEXPLOSION = pygame.mixer.Sound(path.join(SOUND_DIR, 'MysteryExplosion.wav'))
MYSTERYEXPLOSION.set_volume(SOUNDVOLUME)

PLAYERHIT = pygame.mixer.Sound(path.join(SOUND_DIR, 'PlayerHit.wav'))
PLAYERHIT.set_volume(SOUNDVOLUME)

POWERUPGENERATED = pygame.mixer.Sound(path.join(SOUND_DIR, 'PowerupGenerated.wav'))
POWERUPGENERATED.set_volume(SOUNDVOLUME)

POWERUPPICKEDUP = pygame.mixer.Sound(path.join(SOUND_DIR, 'PowerupPickedUp.wav'))
POWERUPPICKEDUP.set_volume(SOUNDVOLUME)

SATELLITEANNOUNCE = pygame.mixer.Sound(path.join(SOUND_DIR, 'Satelliteannounce.wav'))
SATELLITEANNOUNCE.set_volume(SOUNDVOLUME)

SATELLITEEXPLOSION = pygame.mixer.Sound(path.join(SOUND_DIR, 'SatelliteExplosion.wav'))
SATELLITEEXPLOSION.set_volume(SOUNDVOLUME)

# sprite groups
alien_group = pygame.sprite.Group()
alien_backup_group = pygame.sprite.Group()
alien_elite_group = pygame.sprite.Group()
alien_master_group = pygame.sprite.Group()
alien_reinforcements_group = pygame.sprite.Group()
alien_laser_group = pygame.sprite.Group()

barrier_master_group = pygame.sprite.Group()
boss_group = pygame.sprite.Group()
bossvessel_group = pygame.sprite.Group()
bossvessel_laser_group = pygame.sprite.Group()

explosion_group = pygame.sprite.Group()

laser_group = pygame.sprite.Group()

mystery_group = pygame.sprite.Group()

player_group = pygame.sprite.Group()

powerup_group = pygame.sprite.Group()

satellite_group = pygame.sprite.Group()
star_group = pygame.sprite.Group()

# miscellaneous

should_aliens_drop = False
should_aliens_move = False

ALIENBACKUPSCORE = 20
ALIENDROP = 20
ALIENELITESCORE = 30
ALIENSCORE = 10
ALIENSPEED = 10
ALIENSTARTYPOS = 65

BOSSVESSELDISTANCE = 90
BOSSVESSELMAXHEALTH = 100

LASERSPEED = 5

MYSTERYSCORE = 100
MYSTERYSPEED = 5
MYSTERYTIME = 10
MYSTERYXPOS = -75

PLAYERSPEED = 7

POWERUPSPEED = 2

SATELLITESPEED = -5
SATELLITETIME = 3
SATELLITEXPOS = WIDTH + 31

STARSPEED = 1

time_last_hit = 0


class Player(pygame.sprite.Sprite):
    # the text in between brackets above ensure my class Player inherits from
    # the pygame Sprite class
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(path.join(IMAGE_DIR, "ship.png")).convert()
        self.image = pygame.transform.scale(self.image, (45, 45))
        self.rect = self.image.get_rect(topleft=(300, 540))
        self.speed = PLAYERSPEED
        self.lives = 5
        self.score = 0
        self.last_shot = pygame.time.get_ticks() / 1000
        self.cool_down = 0.5
        self.radius = 21
        self.double_shot = False

    def update(self, keys):
        if keys[pygame.K_LEFT] and self.rect.left >= 0:
            self.rect.x -= self.speed
        if keys[pygame.K_RIGHT] and self.rect.right <= WIDTH:
            self.rect.x += self.speed


class Alien(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.index = 0
        self.image = alien_images[self.index]
        self.rect = self.image.get_rect()
        self.alien_last_moved = 0
        self.speed = ALIENSPEED

    def update(self, now):

        global should_aliens_drop
        global should_aliens_move

        if now - self.alien_last_moved >= 0.5:
            self.alien_last_moved = now

            if self.rect.right + self.speed >= (WIDTH - 10) or self.rect.left + self.speed <= 10:
                should_aliens_drop = True
            else:
                should_aliens_move = True


class AlienBackup(Alien):
    def __init__(self):
        super().__init__()
        self.image = alien_backup_images[Alien().index]


class AlienElite(Alien):
    def __init__(self):
        super().__init__()
        self.image = alien_elite_images[Alien().index]


class Barrier(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface((5, 5))
        self.image.fill(GREEN)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y

    def update(self, keys, *args):
        pass


class Boss(Alien):
    def __init__(self):
        super().__init__()
        self.index = 0
        self.image = alien_images[self.index]
        self.rect = self.image.get_rect()
        self.alien_last_moved = 0
        self.speed = ALIENSPEED

    def update(self, now, *args):

        global should_aliens_drop
        global should_aliens_move

        if now - self.alien_last_moved >= 0.5:
            self.alien_last_moved = now

            if self.rect.right + (bossvessel.width / 2) + self.speed >= (WIDTH - 10) \
                    or self.rect.left - (bossvessel.width / 2) + self.speed <= 10:
                should_aliens_drop = True
            else:
                should_aliens_move = True


class Bossvessel(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.width = 220
        self.height = 200
        self.image = pygame.image.load(path.join(IMAGE_DIR, "boss.png")).convert_alpha()
        self.image = pygame.transform.scale(self.image, (self.width, self.height))  # 11, 10
        self.rect = self.image.get_rect()
        self.health = BOSSVESSELMAXHEALTH

    def update(self):
        self.rect.centerx = boss.rect.centerx
        self.rect.centery = boss.rect.centery + BOSSVESSELDISTANCE


class ExplosionBlue(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(path.join(IMAGE_DIR, "explosionblue.png")).convert_alpha()
        self.image = pygame.transform.scale(self.image, (45, 45))
        self.rect = self.image.get_rect()
        self.rect.x = x - 5
        self.rect.y = y - 7

        self.timer = pygame.time.get_ticks()

    def update(self, current_time):
        passed = (current_time * 1000) - self.timer
        if passed >= 200:
            self.kill()


class ExplosionGreen(ExplosionBlue):
    def __init__(self, x, y):
        super().__init__(x, y)
        self.image = pygame.image.load(path.join(IMAGE_DIR, "explosiongreen.png")).convert_alpha()
        self.image = pygame.transform.scale(self.image, (45, 45))


class ExplosionPurple(ExplosionBlue):
    def __init__(self, x, y):
        super().__init__(x, y)
        self.image = pygame.image.load(path.join(IMAGE_DIR, "explosionpurple.png")).convert_alpha()
        self.image = pygame.transform.scale(self.image, (45, 45))


class ExplosionRed(ExplosionBlue):
    def __init__(self, x, y):
        super().__init__(x, y)
        self.image = pygame.image.load(path.join(IMAGE_DIR, "explosionred.png")).convert_alpha()
        self.image = pygame.transform.scale(self.image, (45, 45))


class Laser(pygame.sprite.Sprite):
    def __init__(self, x, y, speed, colour):
        pygame.sprite.Sprite.__init__(self)
        self.width = 2
        self.height = 4
        self.image = pygame.Surface((self.width, self.height))
        self.colour = colour
        self.image.fill(self.colour)
        self.rect = self.image.get_rect()
        self.speed = speed
        self.rect.centerx = x
        self.rect.centery = y

    def update(self):
        self.rect.y -= self.speed
        if self.rect.y + (self.height / 2) <= 0 or self.rect.y - (self.height / 2) >= HEIGHT:
            self.kill()


class Mystery(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.width = 75
        self.height = 35
        self.image = pygame.image.load(path.join(IMAGE_DIR, "mystery.png")).convert_alpha()
        self.image = pygame.transform.scale(self.image, (self.width, self.height))
        self.rect = self.image.get_rect()
        self.speed = MYSTERYSPEED
        self.last_appeared = 0
        self.last_stopped = 0
        self.rect.x = MYSTERYXPOS
        self.rect.y = 25

    def update(self, level, levelstarttime, now):
        if len(mystery_group) == 1:
            self.rect.x += self.speed
            # Past lvl 3 when hit after it never returns due to speed never being reset to 5
            if not ((WIDTH / 2) - 2 <= self.rect.centerx <= (WIDTH / 2) + 2):
                self.speed = MYSTERYSPEED

        if self.rect.x >= WIDTH:
            self.last_stopped = 0
            self.kill()

        if 11 > level >= 6 and 60 >= (now - levelstarttime) >= 30:
            if self.speed == 0 and (now - self.last_stopped) >= 3 and len(mystery_group) == 1 \
                    and (WIDTH / 2) - 2 <= self.rect.centerx <= (WIDTH / 2) + 2:
                generate_alien_reinforcements()
                ALIENAPPEAR.play()
                self.rect.centerx = (WIDTH / 2) + 3
                self.speed = MYSTERYSPEED
                self.last_appeared = now
                self.rect.x = (WIDTH / 2) + 6
            if self.speed != 0 and (WIDTH / 2) - 2 <= self.rect.centerx <= (WIDTH / 2) + 2:
                self.speed = 0
                self.last_stopped = now


class Powerup(pygame.sprite.Sprite):
    def __init__(self, x, y, colour):
        pygame.sprite.Sprite.__init__(self)
        self.width = 4
        self.height = 20
        self.image = pygame.Surface((self.width, self.height))
        self.colour = colour
        self.image.fill(self.colour)
        self.rect = self.image.get_rect()
        self.speed = POWERUPSPEED
        self.generated_this_level = False
        self.rect.centerx = x
        self.rect.y = y

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

        if self.rect.top >= HEIGHT:
            self.kill()


class Satellite(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(path.join(IMAGE_DIR, "satellite.png")).convert_alpha()
        self.image = pygame.transform.scale(self.image, (106, 40))
        self.rect = self.image.get_rect()
        self.speed = SATELLITESPEED
        self.rect.x = SATELLITEXPOS
        self.rect.y = 25
        self.powerup_generated = False
        self.last_stopped = 0
        self.stopped_this_level = False

    def update(self, now):
        self.rect.x += self.speed

        if self.rect.right <= 0:
            self.kill()

        if len(satellite_group) > 0 and self.rect.right <= WIDTH and self.rect.left >= 0 \
                and not self.stopped_this_level:
            if random.randint(1, 200) == 1:
                self.last_stopped = pygame.time.get_ticks() / 1000
                self.speed = 0
                self.stopped_this_level = True

        if 2 > now - self.last_stopped > 1 and len(powerup_group) == 0:
            generate_powerup()
            POWERUPGENERATED.play()
            self.powerup_generated = True

        if now - self.last_stopped >= 3:
            self.speed = SATELLITESPEED


class Star(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface([1, 1])
        self.image.fill(WHITE)
        self.rect = self.image.get_rect()
        self.speed = STARSPEED

    def update(self):
        self.rect.y += self.speed
        if self.rect.y >= HEIGHT:
            self.rect.y = 0


# functions
def alien_drop():
    for alien in alien_master_group:
        alien.rect.y += ALIENDROP
        alien.speed *= -1


def alien_move():
    if len(boss_group) == 0:
        for alien in alien_master_group:
            alien.rect.x += alien.speed
        for alien in alien_group:
            alien.index += 1
            if alien.index >= 2:
                alien.index = 0
            alien.image = alien_images[alien.index]
        for alien in alien_backup_group:
            alien.index += 1
            if alien.index >= 2:
                alien.index = 0
            alien.image = alien_backup_images[alien.index]
        for alien in alien_elite_group:
            alien.index += 1
            if alien.index >= 2:
                alien.index = 0
            alien.image = alien_elite_images[alien.index]
    else:
        for alien in alien_master_group:
            alien.rect.x += alien.speed
        for alien in alien_group:
            alien.index += 1
            if alien.index >= 2:
                alien.index = 0
            alien.image = alien_images[alien.index]


def alien_shoot():
    if len(alien_laser_group) <= math.trunc(len(alien_group) / 10) and len(alien_group) > 0:
        alien = random.choice(list(alien_group))
        laser = Laser(alien.rect.centerx, alien.rect.centery, -LASERSPEED, BLUE)
        alien_laser_group.add(laser)

    if len(alien_laser_group) <= math.trunc(len(alien_backup_group) / 5) and len(alien_backup_group) > 0:
        alien = random.choice(list(alien_backup_group))
        laser = Laser(alien.rect.centerx, alien.rect.centery, -LASERSPEED, GREEN)
        alien_laser_group.add(laser)

    if len(alien_laser_group) <= math.trunc(len(alien_elite_group) / 2) and len(alien_elite_group) > 0:
        alien = random.choice(list(alien_elite_group))
        laser = Laser(alien.rect.centerx, alien.rect.centery, -LASERSPEED, PURPLE)
        alien_laser_group.add(laser)

    if len(alien_laser_group) <= math.trunc(len(alien_reinforcements_group) / 2) and len(
            alien_reinforcements_group) > 0:
        alien = random.choice(list(alien_reinforcements_group))
        laser = Laser(alien.rect.centerx, alien.rect.centery, -LASERSPEED, PURPLE)
        alien_laser_group.add(laser)


def bossvessel_shoot():
    if len(bossvessel_group) > 0 and bossvessel.health > 0 and len(bossvessel_laser_group) < random.randint(0, 9):
        laser = Laser(bossvessel.rect.left + (random.randint(0, 6) * 35), bossvessel.rect.y + bossvessel.height,
                      -LASERSPEED, RED)
        bossvessel_laser_group.add(laser)


def check_collisions(now, bossgrouplen):
    global time_last_hit

    for alien in alien_master_group:
        for barrier in barrier_master_group:
            if alien.rect.colliderect(barrier.rect):
                barrier.kill()

        if alien.rect.colliderect(player.rect):
            explosion_group.add(ExplosionGreen(alien.rect.x, alien.rect.y))
            explosion_group.add(ExplosionGreen(player.rect.x, player.rect.y))
            player.lives = 0
            alien.kill()
            ALIENEXPLOSIONSOUND.play()
            player.kill()

    pygame.sprite.groupcollide(alien_laser_group, barrier_master_group, True, True)
    pygame.sprite.groupcollide(bossvessel_laser_group, barrier_master_group, True, True)
    pygame.sprite.groupcollide(laser_group, barrier_master_group, True, True)

    for laser in laser_group:
        for alien in alien_group:
            if laser.rect.colliderect(alien.rect):
                explosion_group.add(ExplosionBlue(alien.rect.x, alien.rect.y))
                player.score += ALIENSCORE
                laser.kill()
                alien.kill()
                ALIENEXPLOSIONSOUND.play()
                time_last_hit = now

        for alien in alien_backup_group:
            if laser.rect.colliderect(alien.rect):
                explosion_group.add(ExplosionGreen(alien.rect.x, alien.rect.y))
                player.score += ALIENBACKUPSCORE
                laser.kill()
                alien.kill()
                ALIENEXPLOSIONSOUND.play()
                time_last_hit = now

        for alien in alien_elite_group:
            if laser.rect.colliderect(alien.rect):
                explosion_group.add(ExplosionPurple(alien.rect.x, alien.rect.y))
                player.score += ALIENELITESCORE
                laser.kill()
                alien.kill()
                ALIENEXPLOSIONSOUND.play()
                time_last_hit = now

        for alien in alien_reinforcements_group:
            if laser.rect.colliderect(alien.rect):
                explosion_group.add(ExplosionPurple(alien.rect.x, alien.rect.y))
                player.score += ALIENELITESCORE
                laser.kill()
                alien.kill()
                ALIENEXPLOSIONSOUND.play()
                time_last_hit = now

        if bossgrouplen > 0:
            if laser.rect.colliderect(boss.rect):
                laser.kill()
                time_last_hit = now

            if laser.rect.colliderect(bossvessel.rect):
                bossvessel.health -= 1
                if bossvessel.health <= 0:
                    explosion_group.add(ExplosionGreen(boss.rect.centerx, boss.rect.y))
                    boss.kill()
                    explosion_group.add(ExplosionRed(bossvessel.rect.centerx, bossvessel.rect.y))
                    bossvessel.kill()
                laser.kill()
                time_last_hit = now

        if laser.rect.colliderect(mystery.rect):
            explosion_group.add(ExplosionRed(mystery.rect.centerx, mystery.rect.y))
            player.score += MYSTERYSCORE
            mystery.rect.x = MYSTERYXPOS
            laser.kill()
            mystery.kill()
            MYSTERYEXPLOSION.play()
            time_last_hit = now

        if laser.rect.colliderect(satellite.rect):
            explosion_group.add(ExplosionBlue(satellite.rect.centerx, satellite.rect.y))
            satellite.rect.x = SATELLITEXPOS
            laser.kill()
            satellite.kill()
            SATELLITEEXPLOSION.play()
            time_last_hit = now

    for laser in alien_laser_group:
        hits = pygame.sprite.spritecollide(laser, player_group, False, pygame.sprite.collide_circle)
        for hit in hits:
            player_hit()
            laser.kill()
            time_last_hit = now
            if player.lives == 0:
                explosion_group.add(ExplosionGreen(player.rect.x, player.rect.y))
                player.kill()

    for laser in bossvessel_laser_group:
        hits = pygame.sprite.spritecollide(laser, player_group, False, pygame.sprite.collide_circle)
        for hit in hits:
            player_hit()
            laser.kill()
            time_last_hit = now
            if player.lives == 0:
                explosion_group.add(ExplosionGreen(player.rect.x, player.rect.y))
                player.kill()

    for powerup in powerup_group:
        if powerup.rect.colliderect(player.rect):
            powerup.kill()
            POWERUPPICKEDUP.play()
            if player.lives < 5:
                randnumber = random.randint(1, 3)
                if randnumber == 1:
                    player.double_shot = True
                elif randnumber == 2:
                    num = random.randint(0, 3)
                    generate_barrier(num, num + 1)
                else:
                    player.lives += 1
            elif player.lives == 5:
                randnumber = random.randint(1, 2)
                if randnumber == 1:
                    player.double_shot = True
                else:
                    num = random.randint(0, 3)
                    generate_barrier(num, num + 1)


def draw_boss_health_bar(screen):
    pygame.draw.rect(screen, RED, pygame.Rect(5, HEIGHT - 10, WIDTH - 10, 10), 2)

    pygame.draw.rect(screen, RED,
                     pygame.Rect(5, HEIGHT - 10, (WIDTH - 10) * (bossvessel.health / BOSSVESSELMAXHEALTH), 10))


def draw_lives(surf, x, y, lives, img):
    for i in range(lives):
        img_rect = img.get_rect()
        img_rect.x = x + 30 * i
        img_rect.y = y
        surf.blit(img, img_rect)


def draw_text(surf, font, size, text, colour, x, y, location):
    font = pygame.font.Font(font, size)
    text_surface = font.render(text, True, colour)
    text_rect = text_surface.get_rect()
    if location == 'center':
        text_rect.center = (x, y)
    elif location == 'left':
        text_rect.midleft = (x, y)
    surf.blit(text_surface, text_rect)


def generate_barrier(num1, num2):
    for i in range(num1, num2):
        for x in range(20):
            for y in range(10):
                barrier = Barrier(50 + (x * 5) + (200 * i), 525 - (y * 5))
                barrier_master_group.add(barrier)


def generate_aliens_lvl1():
    for x in range(10):
        for y in range(2):
            alien = Alien()
            alien.rect.x = 50 + (x * 50)
            alien.rect.y = ALIENSTARTYPOS + (y * 45)
            alien_group.add(alien)
            alien_master_group.add(alien)


def generate_aliens_lvl2():
    for x in range(10):
        for y in range(4):
            if y in (0, 1):
                alien = AlienBackup()
                alien.rect.x = 50 + (x * 50)
                alien.rect.y = ALIENSTARTYPOS + (y * 45)
                alien_backup_group.add(alien)
                alien_master_group.add(alien)
            elif y in (2, 3):
                alien = Alien()
                alien.rect.x = 50 + (x * 50)
                alien.rect.y = ALIENSTARTYPOS + (y * 45)
                alien_group.add(alien)
                alien_master_group.add(alien)


def generate_aliens_lvl3():
    for x in range(10):
        for y in range(5):
            if y == 0:
                alien = AlienElite()
                alien.rect.x = 50 + (x * 50)
                alien.rect.y = ALIENSTARTYPOS + (y * 45)
                alien_elite_group.add(alien)
                alien_master_group.add(alien)
            elif y in (1, 2):
                alien = AlienBackup()
                alien.rect.x = 50 + (x * 50)
                alien.rect.y = ALIENSTARTYPOS + (y * 45)
                alien_backup_group.add(alien)
                alien_master_group.add(alien)
            elif y in (3, 4):
                alien = Alien()
                alien.rect.x = 50 + (x * 50)
                alien.rect.y = ALIENSTARTYPOS + (y * 45)
                alien_group.add(alien)
                alien_master_group.add(alien)


def generate_alien_reinforcements():
    for x in range(5):
        alien = AlienElite()
        alien.rect.x = ((WIDTH / 2) - 120) + (x * 50)
        alien.rect.y = ALIENSTARTYPOS
        alien_reinforcements_group.add(alien)
        alien_master_group.add(alien)


def generate_boss():
    boss.rect.centerx = WIDTH / 2
    boss.rect.y = ALIENSTARTYPOS
    alien_group.add(boss)
    alien_master_group.add(boss)
    boss_group.add(boss)


def generate_boss_vessel():
    bossvessel.rect.centerx = boss.rect.centerx
    bossvessel.rect.centery = boss.rect.centery + BOSSVESSELDISTANCE
    bossvessel_group.add(bossvessel)


def generate_mystery():
    mystery.rect.x = MYSTERYXPOS
    mystery_group.add(mystery)
    mystery.last_appeared = pygame.time.get_ticks() / 1000


def generate_powerup():
    powerup = Powerup(satellite.rect.centerx, satellite.rect.centery, ORANGE)
    if powerup.generated_this_level is False:
        powerup.generated_this_level = True
        powerup_group.add(powerup)


def generate_satellite():
    satellite.rect.x = SATELLITEXPOS
    satellite_group.add(satellite)


def generate_stars():
    for i in range(random.randint(80, 120)):
        x = random.randint(1, WIDTH - 1)
        y = random.randint(1, HEIGHT - 1)
        star = Star()
        star.rect.x = x
        star.rect.y = y

        star_group.add(star)


def intermission_screen(screen, image_name, image_scale_x, image_scale_y, screen_pos_x, screen_pos_y,
                        text, text_pos_x, text_pos_y, text_loc):
    load_screen_image = pygame.image.load(path.join(IMAGE_DIR, image_name)).convert_alpha()
    image = pygame.transform.scale(load_screen_image, (image_scale_x, image_scale_y))
    screen.blit(image, (screen_pos_x, screen_pos_y))
    draw_text(screen, FONT, 52, text, WHITE, text_pos_x, text_pos_y, text_loc)


def pauze_background_music():
    pygame.mixer.music.pause()


def play_background_music():
    if random.randint(1, 2) == 1:
        pygame.mixer.music.load(BACKGROUND1)
    else:
        pygame.mixer.music.load(BACKGROUND2)
    pygame.mixer.music.set_volume(MUSICVOLUME)
    pygame.mixer.music.play(-1)


def player_hit():
    player.lives -= 1
    PLAYERHIT.play()


def unpauze_background_music():
    pygame.mixer.music.unpause()


# miscellaneous
player = Player()
player_group.add(player)

boss = Boss()
bossvessel = Bossvessel()
mystery = Mystery()
satellite = Satellite()


class SpaceInvaders(object):
    def __init__(self):
        pygame.init()
        self.clock = pygame.time.Clock()
        self.fps = self.clock.get_fps()
        self.screen = SCREEN
        self.now = pygame.time.get_ticks() / 1000
        self.levelstarttime = 0
        self.level = 1

        self.menu()

    def game_over(self, endgame):
        pygame.mixer.music.stop()
        if endgame == 0:
            endgametext = "GAME OVER"
            endgamecolour = RED
        else:
            endgametext = "VICTORY"
            endgamecolour = GREEN

        pygame.time.wait(2000)
        draw_text(self.screen, FONT, 64, endgametext, endgamecolour, WIDTH / 2, HEIGHT / 2, 'center')
        GAMEOVER.play()
        pygame.display.update()

        pygame.time.wait(2000)
        draw_text(self.screen, FONT, 32, "Press space bar to restart", WHITE, WIDTH / 2, (HEIGHT / 2) + 50, 'center')
        draw_text(self.screen, FONT, 32, "or press escape to quit", WHITE, WIDTH / 2, (HEIGHT / 2) + 100, 'center')
        pygame.display.update()

        pygame.time.wait(2000)
        SCREEN.fill(BLACK)
        draw_text(self.screen, FONT, 64, endgametext, endgamecolour, WIDTH / 2, HEIGHT / 2, 'center')
        draw_text(self.screen, FONT, 32, "Press space bar to restart", WHITE, WIDTH / 2, (HEIGHT / 2) + 50, 'center')
        draw_text(self.screen, FONT, 32, "or press escape to quit", WHITE, WIDTH / 2, (HEIGHT / 2) + 100, 'center')
        pygame.display.update()

        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_SPACE:
                        self.level = 1
                        self.new_game()
                    if event.key == pygame.K_ESCAPE:
                        pygame.quit()
                        sys.exit()

    def intermission(self):
        pauze_background_music()
        pygame.time.wait(2000)
        self.screen.fill(BLACK)

        if 11 > self.level >= 1:
            intermission_screen(self.screen, "ship.png", 45, 45, 50, (HEIGHT * 0.25) + 50,
                                '= Player', 100, (HEIGHT * 0.25) + 75, 'left')

            intermission_screen(self.screen, "enemy2_1.png", 45, 45, (WIDTH / 2) + 50, (HEIGHT * 0.25) + 50,
                                '= ' + str(ALIENSCORE) + ' points', (WIDTH / 2) + 100, (HEIGHT * 0.25) + 75, 'left')

        if 11 > self.level >= 2:
            intermission_screen(self.screen, "enemy3_2.png", 45, 45, 50, (HEIGHT * 0.25) + 150,
                                '= ' + str(ALIENBACKUPSCORE) + ' points', 100, (HEIGHT * 0.25) + 175, 'left')

        if 11 > self.level >= 3:
            intermission_screen(self.screen, "enemy1_1.png", 45, 45, (WIDTH / 2) + 50, (HEIGHT * 0.25) + 150
                                , '= ' + str(ALIENELITESCORE) + ' points', (WIDTH / 2) + 100, (HEIGHT * 0.25) + 175, 'left')

        if 11 > self.level >= 4:
            intermission_screen(self.screen, "mystery.png", 75, 35, 35, (HEIGHT * 0.25) + 250,
                                '= ' + str(MYSTERYSCORE) + ' points', 110, (HEIGHT * 0.25) + 275, 'left')

        if 11 > self.level >= 5:
            intermission_screen(self.screen, "satellite.png", 106, 40, (WIDTH / 2) + 20, (HEIGHT * 0.25) + 250
                                , "= don't shoot", (WIDTH / 2) + 125, (HEIGHT * 0.25) + 275, 'left')

            intermission_screen(self.screen, "powerup.png", 8, 40, (WIDTH / 2) + 75, (HEIGHT * 0.25) + 350
                                , "= powerup", (WIDTH / 2) + 100, (HEIGHT * 0.25) + 375, 'left')

        if self.level == 6:
            draw_text(self.screen, FONT, 52, 'Watch out for reinforcements', RED, WIDTH / 2, 50, 'center')

        if self.level <= 10:
            draw_text(self.screen, FONT, 52, 'level ' + str(int(self.level)), GREEN, WIDTH / 2, (HEIGHT * 0.25), 'center')
        elif self.level == 11:
            draw_text(self.screen, FONT, 52, 'FINAL BOSS', RED, WIDTH / 2, (HEIGHT / 2), 'center')

        pygame.display.update()
        pygame.time.wait(3000)
        unpauze_background_music()

    def menu(self):

        generate_stars()

        while True:

            button = pygame.Rect((WIDTH / 2) - 100, (HEIGHT / 2), 200, 100)

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
                if event.type == pygame.MOUSEBUTTONDOWN:
                    if event.button == 1:
                        if button.collidepoint(event.pos):
                            pygame.time.wait(2000)
                            self.intermission()
                            self.new_game()
                if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                    self.intermission()
                    self.new_game()

            keys = pygame.key.get_pressed()

            self.screen.fill(BLACK)

            pygame.draw.rect(self.screen, BLACK, button)

            star_group.update()
            star_group.draw(self.screen)

            draw_text(self.screen, STARTFONT, 64, 'START', WHITE, button.centerx, button.centery, 'center')

            draw_text(self.screen, LOGOFONT, 80, 'SPACE INVADERS', GREEN, WIDTH / 2, (HEIGHT / 2) - 100, 'center')

            draw_text(self.screen, FONT, 25, 'Or press SPACE to start the game', WHITE, WIDTH / 2,
                      (HEIGHT - 100), 'center')

            draw_text(self.screen, FONT, 25, 'Use arrow keys to move and space bar to shoot', WHITE, WIDTH / 2,
                      (HEIGHT - 50), 'center')

            pygame.display.flip()
            self.clock.tick(FPS)

    def new_game(self):
        global alien_group
        alien_group = pygame.sprite.Group()
        global alien_backup_group
        alien_backup_group = pygame.sprite.Group()
        global alien_elite_group
        alien_elite_group = pygame.sprite.Group()
        global alien_master_group
        alien_master_group = pygame.sprite.Group()
        global alien_reinforcements_group
        alien_reinforcements_group = pygame.sprite.Group()
        global alien_laser_group
        alien_laser_group = pygame.sprite.Group()
        global boss_group
        boss_group = pygame.sprite.Group()
        global bossvessel_group
        bossvessel_group = pygame.sprite.Group()
        global explosion_group
        explosion_group = pygame.sprite.Group()
        global laser_group
        laser_group = pygame.sprite.Group()
        global mystery_group
        mystery_group = pygame.sprite.Group()
        global powerup_group
        powerup_group = pygame.sprite.Group()
        global satellite_group
        satellite_group = pygame.sprite.Group()

        if self.level == 1:
            global barrier_master_group
            barrier_master_group = pygame.sprite.Group()
            global player_group
            player_group = pygame.sprite.Group()
            global star_group
            star_group = pygame.sprite.Group()
            global player
            player = Player()
            player_group.add(player)
            player.lives = 5
            player.score = 0

            generate_aliens_lvl1()

        elif self.level == 2:
            generate_aliens_lvl2()
        elif 11 > self.level >= 3:
            generate_aliens_lvl3()

        if self.level <= 3:
            generate_barrier(0, 4)

        if self.level == 11:
            generate_boss()
            generate_boss_vessel()

        generate_stars()

        play_background_music()

        mystery.last_appeared = pygame.time.get_ticks() / 1000
        satellite.appeared_this_level = False

        self.game_loop()

    def game_start(self):
        self.menu()

    def game_loop(self):
        while True:

            self.now = (pygame.time.get_ticks() / 1000)

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
                elif event.type == pygame.K_SPACE and player.lives == 0:
                    SpaceInvaders()

            global should_aliens_drop
            should_aliens_drop = False

            global should_aliens_move
            should_aliens_move = False

            keys = pygame.key.get_pressed()

            if keys[pygame.K_SPACE]:
                if player.lives > 0:
                    if self.now - player.last_shot >= player.cool_down and not player.double_shot:
                        player.last_shot = self.now
                        laser = Laser(player.rect.centerx, player.rect.y, LASERSPEED, GREEN)
                        laser_group.add(laser)
                        LASERSOUND.play()
                    if self.now - player.last_shot >= player.cool_down and player.double_shot:
                        player.last_shot = self.now
                        laser = Laser(player.rect.centerx - 15, player.rect.y + 20, LASERSPEED, GREEN)
                        laser_group.add(laser)
                        laser = Laser(player.rect.centerx + 15, player.rect.y + 20, LASERSPEED, GREEN)
                        laser_group.add(laser)
                        LASERSOUND.play(1)

            self.screen.fill(BLACK)

            star_group.update()
            star_group.draw(self.screen)

            alien_master_group.update(self.now)
            alien_master_group.draw(self.screen)

            if should_aliens_drop:
                alien_drop()
                should_aliens_drop = False

            if should_aliens_move:
                alien_move()
                should_aliens_move = False

            alien_laser_group.update()
            alien_laser_group.draw(self.screen)

            barrier_master_group.update(keys)
            barrier_master_group.draw(self.screen)

            bossvessel_group.update(keys)
            bossvessel_group.draw(self.screen)

            bossvessel_laser_group.update(keys)
            bossvessel_laser_group.draw(self.screen)

            if self.level >= 10 and len(boss_group) == 1:
                draw_boss_health_bar(self.screen)

            explosion_group.update(self.now)
            explosion_group.draw(self.screen)

            laser_group.update()
            laser_group.draw(self.screen)

            mystery_group.update(self.level, self.levelstarttime, self.now)
            mystery_group.draw(self.screen)

            if self.level >= 4 and len(mystery_group) == 0 and (self.now - mystery.last_appeared) >= MYSTERYTIME:
                generate_mystery()

            player_group.update(keys)
            player_group.draw(self.screen)

            powerup_group.update(keys)
            powerup_group.draw(self.screen)

            satellite_group.update(self.now)
            satellite_group.draw(self.screen)

            if self.level >= 5 and len(satellite_group) == 0 and random.randint(1, 750) == 1 \
                    and not satellite.powerup_generated:
                generate_satellite()
                SATELLITEANNOUNCE.play()

            check_collisions(self.now, len(boss_group))

            # print(len(bossvessel_group))

            draw_text(self.screen, FONT, 32, 'FPS ' + str(int(self.clock.get_fps())), WHITE, WIDTH - 60, 14, 'center')

            draw_text(self.screen, FONT, 32, 'SCORE ' + str(int(player.score)), WHITE, WIDTH / 2, 14, 'center')

            draw_lives(self.screen, 10, 5, player.lives, pygame.transform.scale(player.image, (18, 18)))

            if player.lives == 0 and self.now - time_last_hit >= 2:
                self.game_over(len(player_group))
            if len(alien_master_group) == 0 and self.now - time_last_hit >= 2:
                satellite.stopped_this_level = False
                self.level += 1
                self.levelstarttime = self.now
                player.double_shot = False
                self.intermission()
                self.new_game()

            if len(boss_group) == 0:
                alien_shoot()
            else:
                bossvessel_shoot()

            pygame.display.flip()
            self.clock.tick(FPS)


if __name__ == '__main__':
    # Make a game instance, and run the game.
    game = SpaceInvaders()
    # game.game_loop()
    game.menu()
\$\endgroup\$
7
  • 1
    \$\begingroup\$ pygame.display.set_icon(pygame.image.load(path.join(IMAGE_DIR, "logo.png")).convert_alpha()) FileNotFoundError: No such file or directory. You'll need to post the assets somewhere (perhaps on github/gitlab?) so we can see how the game runs. \$\endgroup\$
    – C. Harley
    Aug 31, 2021 at 19:22
  • 1
    \$\begingroup\$ I will add a link with all the assets tomorrow \$\endgroup\$ Aug 31, 2021 at 20:37
  • 1
    \$\begingroup\$ Just added a Github link \$\endgroup\$ Sep 1, 2021 at 9:48
  • \$\begingroup\$ Thanks, yes, the code works, I'll look into a review. Small note though, I was hit multiple times but didn't die, and when I did (a hit on the wings), I wasn't able to use any of my ships - it went straight to the end of the game. No matter though, let's look at your code. \$\endgroup\$
    – C. Harley
    Sep 1, 2021 at 21:44
  • \$\begingroup\$ The way I programmed it is that you start with 5 lives (represented by each ship in the upper left corner) and every time you get hit you lose one. \$\endgroup\$ Sep 2, 2021 at 7:08

1 Answer 1

4
\$\begingroup\$

and thanks for your post. From a quick scan of your code, these issues jump out at me:

  • Configurations inside the code instead of in an ini file, breaking the Open/Close Principle,
  • Many instances of MAGIC_STRINGS and MAGIC_VALUES, these should be loaded from a configuration file, and restricted to the class or domain where they're used, not as globals,
  • Not using enums when using common items such as colors,
  • Missing functions for repeated actions, these can be brought into a single call instead, such as alien images and the sound effects,
  • Importing globals inside classes - state is no longer encapsulated to a single location. If classes require injected configuration into them, inherit from another class or bring them in like a mixin,
  • Extended lines of code that require lots of calculation to understand what is happening. Wrap those into a single function which explains in plain English to other coders what the math is attempting to produce,
  • References in classes that inherit Alien() base class, but refer to the parent class when defining index instead of self,
  • Unknown references to .speed in the Sprite class (lines 448, 454, 472),
  • Calling sys.exit in the middle of a while True loop. Make the loop evaluate if the user wants to quit, not inside it,
  • Creation of the sprite groups outside the SpaceInvaders class, then importing the via global, but then overwriting them immediately? Why not just create them inside the class?
  • Lines 444 (#functions) through 815 look like they operate on objects inside the game, yet they're separate (with no reference to self), I assume they're designed as helper functions. The problem with that is, there's no reference being passed to them "as functions" so, how do you really know which alien or laser beam - in the example of check_collisions and my experience of being hit multiple times yet not exploding - is working correctly? Without a proper reference or action being allocated to an object, state can go missing, as I've seen when playing the game.

These are the most obvious issues in terms of code that jump out at me without making this post too long.

Let's go through them.

Configuration

The Open/Close Principle states that your code should be open for extension, but closed for modifications. If you want to change the starting location for the alien ships, you would need to go through your entire program and modify various numbers. If you make a mistake and change a number incorrectly, you would need to go back through your entire program, line by line, to find the specific number that is in error. This is what the principle means. It's part of S.O.L.I.D to help you reduce unnecessary errors and produce code which is cleaner and reduce a lot of the 'WTF?' coders say, when looking at other programmers code.

Enum

Enum came about as it's easier to refer to words than numbers, and using the "." between Color and the color you want, intellisense in your IDE gives you a drop-down to make selection easier. To explain it:

>>> from enum import Enum
>>> class Color(Enum):
...     Black = (0,0,0)
...     Blue = (0, 255, 240)
... 
>>> print(Color.Black)
Color.Black
>>> Color.Blue
<Color.Blue: (0, 255, 240)>
>>> Color.Blue.value
(0, 255, 240)
>>> 

Colors would be defined in the configuration file, imported into the code as an enum, and your code would refer to it then. A reason is, certain computers display certain colors slightly different (hardware chips, glass panels, etc). If you want to have perfect colors across all units, you might have a different configuration file with slightly different values. It's better to use a config file than modify the code for every instance out there.

self.image.fill(GREEN)

Would become:

self.image.fill(Color.Green.value)

Repeated Actions

class ExplosionGreen(ExplosionBlue):
    def __init__(self, x, y):
        super().__init__(x, y)
        self.image = pygame.image.load(path.join(IMAGE_DIR, "explosiongreen.png")).convert_alpha()
        self.image = pygame.transform.scale(self.image, (45, 45))
        

In this example, you're inheriting ExplosionBlue, however you're still doing many of the steps manually. ExplosionGreen, which inherits ExplosionBlue, is a manual class for what should just be instantiating an explosion. Let me demonstrate:

class Explosion(pygame.sprite.Sprite):
    def __init__(self, image_filename, location_x, location_y):
        pygame.sprite.Sprite.__init__(self)
        self.explosion_height = config_explosion_height
        self.explosion_width = config_explosion_width
        self.x_image_overlay = config_x_image_overlay
        self.y_image_overlay = config_y_image_overlay
        self.image = pygame.image.load(load_image_file(image_filename)).convert_alpha()
        self.image = pygame.transform.scale(self.image, (self.explosion_height, self.explosion_width))
        self.rect = self.image.get_rect()
        self.rect.x = location_x - self.x_image_overlay
        self.rect.y = location_y - self.y_image_overlay

        self.timer = pygame.time.get_ticks()

    def update(self, current_time):
        passed = (current_time * config_second_in_ms) - self.timer
        if passed >= config_explosion_timeout:
            self.kill()
        

Now you can create any explosion with a simple:

explosion_green = Explosion(config_explosion_image_green, location_x, location_y)

Making code like:

if alien.rect.colliderect(player.rect):
    explosion_group.add(ExplosionGreen(alien.rect.x, alien.rect.y))
    explosion_group.add(ExplosionGreen(player.rect.x, player.rect.y))

into

for x,y in ((alien.rect.x, alien.rect.y), (player.rect.x, player.rect.y)):
    explosion_group.add(Explosion(config_explosion_image_green, x,y))

Importing globals inside classes

Using global is a bad thing to do because you lose control of variable state, and where a variable can be altered from another location, and you bring that value in, can produce weird outcomes, you can spend days tracking down a bug. Try to break your habit of using it. It also leads to sloppy code such as in new_game:

 def new_game(self):
        global alien_group
        alien_group = pygame.sprite.Group()
        global alien_backup_group
        alien_backup_group = pygame.sprite.Group()
    

You've already spent time creating all these variables at the top of your code, yet you're overwriting them in this statement. Where should the creation take place? The code makes it clear you're not really sure.

Extended lines of code

Seeing code like:

if self.rect.right + self.speed >= (WIDTH - 10) or self.rect.left + self.speed <= 10:
    should_aliens_drop = True
else:
    should_aliens_move = True

makes me go hmm... if.. right of the rectangle plus the speed is greater than the WIDTH.. what's the WIDTH of? [searches entire code for WIDTH] ah, the width of the screen, okay, screen width less 10, or, or the rectangle, left of the rectangle plus speed is less than or equal to 10... okay, they drop else they move. okay...

The code would be better if you had:

 if self.alien_at_max_right() or self.alien_at_max_left():
     should_aliens_drop = True
 else:
     should_aliens_move = True

with these functions of course:

def alien_at_max_right(self):
    return self.rect.right + self.speed >= (config_screen_width - config_screen_edge)
def alien_at_max_left(self):
    return self.rect.left + self.speed <= config_screen_edge
    

This is infinitely better - now I say hmmm... okay, if the alien is at the max right, or at the max left, the alien should drop. Okay, sounds good. Else they move - of course, makes sense.

If another coder wants to dig into the particulars of the math, they can do that. But usually they only read your code to fix a bug or to extend it.

Now, don't go overboard with this - if you only have a single line that is very simple like "okay, the circle is the pi times, yeah I know this" then that should be fine to leave it in - or - if the function is only ever called once in the entire code - there's no need to extract that into it's own function unless it's a horribly long formula. The goal is readability of your code.

References in classes that inherit Alien()

class AlienBackup(Alien):
    def __init__(self):
        super().__init__()
        self.image = alien_backup_images[Alien().index]
    

The problem here is using the index on the parent class instead of the class itself. I'm not sure if you did this intentionally, but if you create a child-class, you should always refer to the state variables of that child class itself and not the state of the parent class, which might be modified by other children, again we're trying to reduce instances of weird behavior. Another small point is you're creating a new Alien() parent class in every call to determine the index.

Unknown references to .speed

def alien_drop():
    for alien in alien_master_group:
        alien.rect.y += ALIENDROP
        alien.speed *= -1
    

alien.speed doesn't exist in the class Sprite. Again, be mindful of which class you're referencing. I suggest improving your IDE, try the community version of PyCharm unless you've got memory constraints (java can be a memory hog).

Calling sys.exit

def game_loop(self):
    while True:

        self.now = (pygame.time.get_ticks() / 1000)

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
                

In this instance, what you should do is create a control variable, which decides if it's time to exit the game loop. Such as:

def game_loop(self):
    play_game = True
    while play_game:
        ...
        if event.type == pygame.QUIT:
            play_game = False
            

when the loop finishes, the game will exit the loop, and you can call pygame.quit() outside the loop (where it's meant to be).

Creation of the sprite groups outside the SpaceInvaders class

We covered this a little earlier when talking about new_game but that was about using global. Here we're discussing the location for creation of the sprite groups. Where would be the most appropriate location?

Currently you create them outside at the start of your code, then you recreate them at the start of the new_game function. Wouldn't it be best to create them during the init of the SpaceInvaders (SI) class?

Of course you can define them outside the SI class as they require many lines of definitions, and instantiate them during the init.

Do they require a reset? That functionality can be added to the base class, removing the commands individually from their init, and placed into a def _reset(self): function, which init will call. I hope this point is isn't too confusing?

Lines 444 (#functions) through 815

These functions are part of certain objects - like the aliens themselves. alien_drop() alien_move() alien_shoot() etc. They belong in the alien class as methods.

Drawing lives, generating the barriers, writing the text - they're all part of the game - and should be created as part of the SpaceInvader class (the game engine itself).

It's important for operations that belong to objects stay with the objects to help understand the links between them, and avoid unfortunate references, should someone accidently reference (contrived example here): alien_shoot(player) where an alien would shoot upwards and destroy another alien.

Ending Comments

So Dreadnought, I hope most of this makes sense? If not, I can suggest a book named "Code Complete" which can show you a lot of these mistakes and it can help to improve your coding, if it's something that you enjoy.

Nevertheless, it's a good effort, I had fun playing your game. Please keep it up, and see if you can incorporate these lessons into your code.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.