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()
asked Aug 31, 2021 at 15:04
\$\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\$ Commented Aug 31, 2021 at 19:22
  • 1
    \$\begingroup\$ I will add a link with all the assets tomorrow \$\endgroup\$ Commented Aug 31, 2021 at 20:37
  • 1
    \$\begingroup\$ Just added a Github link \$\endgroup\$ Commented 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\$ Commented 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\$ Commented 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.

answered Sep 2, 2021 at 12:44
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.