From 1004be0b0b312aebc04dc9744b284211c1d55a6f Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Wed, 6 Sep 2023 21:14:21 +0100 Subject: [PATCH 1/6] Added controller rendering order --- classes/Controller.py | 7 - classes/Game_controller.py | 202 +++++------------- classes/{ => controllers}/Audio_controller.py | 12 +- .../{ => controllers}/Baseline_controller.py | 43 ++-- .../{bomb => controllers}/Bomb_controller.py | 44 ++-- classes/{ => controllers}/Input_controller.py | 3 +- .../Invader_controller.py | 38 ++-- .../Mothership_controller.py | 37 ++-- .../Player_controller.py | 14 +- .../Player_missile_controller.py | 21 +- .../Scoreboard_controller.py | 7 +- .../Shield_controller.py | 24 +-- classes/{ => controllers}/UI_controller.py | 6 +- classes/mothership/Mothership.py | 2 - game.py | 1 + lib/Controller.py | 43 ++++ {classes => lib}/Event_manager.py | 0 test.py | 125 +++++++++++ 18 files changed, 360 insertions(+), 269 deletions(-) delete mode 100644 classes/Controller.py rename classes/{ => controllers}/Audio_controller.py (64%) rename classes/{ => controllers}/Baseline_controller.py (52%) rename classes/{bomb => controllers}/Bomb_controller.py (76%) rename classes/{ => controllers}/Input_controller.py (95%) rename classes/{invader => controllers}/Invader_controller.py (74%) rename classes/{mothership => controllers}/Mothership_controller.py (75%) rename classes/{player => controllers}/Player_controller.py (66%) rename classes/{player => controllers}/Player_missile_controller.py (75%) rename classes/{ => controllers}/Scoreboard_controller.py (54%) rename classes/{shield => controllers}/Shield_controller.py (88%) rename classes/{ => controllers}/UI_controller.py (92%) create mode 100644 lib/Controller.py rename {classes => lib}/Event_manager.py (100%) create mode 100644 test.py diff --git a/classes/Controller.py b/classes/Controller.py deleted file mode 100644 index f14b690..0000000 --- a/classes/Controller.py +++ /dev/null @@ -1,7 +0,0 @@ -from classes.Event_manager import EventManager - - -class Controller: - def __init__(self, config): - self.event_manager = EventManager.get_instance() - self.config = config diff --git a/classes/Game_controller.py b/classes/Game_controller.py index 9ee22dd..631cfda 100644 --- a/classes/Game_controller.py +++ b/classes/Game_controller.py @@ -1,20 +1,10 @@ +import importlib +import inspect +import os import pygame import sys -from classes.Controller import Controller -from classes.invader.Invader_controller import InvaderController -from classes.bomb.Bomb_controller import BombController -from classes.player.Player import Player -from classes.player.Player_controller import PlayerController -from classes.player.Player_missile_controller import PlayerMissileController -from classes.shield.Shield_controller import ShieldController -from classes.player.Player_missile import PlayerMissile -from classes.Baseline_controller import BaselineController -from classes.Input_controller import InputController -from classes.Scoreboard_controller import ScoreboardController -from classes.UI_controller import UIController -from classes.mothership.Mothership_controller import MothershipController -from classes.Audio_controller import AudioController +from lib.Controller import Controller class GameController(Controller): @@ -25,7 +15,7 @@ def __init__(self, config): self.player_on_screen = False self.player_missile = None - self.invader_swarm_complete = False + # self.invader_swarm_complete = False self.top_left = config.get("top_left") self.original_screen_size = config.get("original_screen_size") self.larger_screen_size = config.get("larger_screen_size") @@ -37,146 +27,57 @@ def __init__(self, config): self.window_surface = pygame.display.set_mode(self.larger_screen_size) self.max_fps = config.get("max_fps") - self.setup_controllers(config) - self.setup_controller_callbacks() - self.setup_game_events() - - def setup_controllers(self, config): - self.controllers = { - "invader": InvaderController(config), - "mothership": MothershipController(config), - "player": PlayerController(config), - "shield": ShieldController(config), - "missile": PlayerMissileController(), - "bomb": BombController(config), - "baseline": BaselineController(), - "input": InputController(config), - "score": ScoreboardController(config), - "ui": UIController(config), - "audio": AudioController(config), - } - - def setup_controller_callbacks(self): - self.controllers["baseline"].get_bombs_callback = self.controllers[ - "bomb" - ].get_bombs - - self.controllers["bomb"].get_invaders_callback = self.controllers[ - "invader" - ].get_invaders - - self.controllers["bomb"].get_player_callback = self.controllers[ - "player" - ].get_player - - self.controllers["shield"].get_bombs_callback = self.controllers[ - "bomb" - ].get_bombs - - self.controllers["shield"].get_missile_callback = self.controllers[ - "missile" - ].get_player_missile - - self.controllers["shield"].get_invaders_callback = self.controllers[ - "invader" - ].get_invaders - - self.controllers["missile"].get_player_callback = self.controllers[ - "player" - ].get_player - - self.controllers["missile"].mothership_is_exploding = self.controllers[ - "mothership" - ].mothership_is_exploding - - self.controllers["missile"].get_invaders_callback = self.controllers[ - "invader" - ].get_invaders - - self.controllers["ui"].get_score_callback = self.controllers["score"].get_score - - self.controllers["mothership"].get_score_text_callback = self.controllers[ - "ui" - ].create_text_surface - - self.controllers["mothership"].get_invader_count_callback = self.controllers[ - "invader" - ].get_invader_count - - self.controllers["mothership"].get_lowest_invader_y_callback = self.controllers[ - "invader" - ].get_lowest_invader_y - - self.controllers["mothership"].get_missile_callback = self.controllers[ - "missile" - ].get_player_missile - - def setup_game_events(self): - self.event_manager.add_listener("swarm_complete", self.on_swarm_complete) - self.event_manager.add_listener( - "mothership_spawned", self.controllers["audio"].on_mothership_spawned - ) - - self.event_manager.add_listener( - "mothership_hit", self.controllers["audio"].on_mothership_bonus + "escape_button_pressed", self.on_escape_button_pressed ) - self.event_manager.add_listener( - "mothership_hit", self.controllers["score"].on_points_awarded - ) + self.setup_game_events() - self.event_manager.add_listener( - "mothership_exit", self.controllers["audio"].on_mothership_exit - ) + def debug_controllers(self): + print("Ordered Controllers:") + for controller in self.controllers: + print( + f"{controller.__class__.__name__} - Rendering Order: {controller.rendering_order}" + ) - self.event_manager.add_listener( - "points_awarded", self.controllers["score"].on_points_awarded - ) + def load_controllers(self, config): + # Initialize an empty list to store the imported controllers + self.controllers = [] - self.event_manager.add_listener( - "invader_hit", self.controllers["invader"].on_invader_hit - ) - self.event_manager.add_listener( - "invader_removed", self.controllers["missile"].on_missile_ready - ) + # Construct the full directory path for controllers + controllers_directory = os.path.join("classes", "controllers") - self.event_manager.add_listener( - "play_delay_complete", self.controllers["player"].on_play_delay_complete - ) + # Loop through files in the controllers directory + for filename in os.listdir(controllers_directory): + if filename.endswith("_controller.py"): + # Extract the module name without the extension + module_name = filename[:-3] - self.event_manager.add_listener( - "play_delay_complete", self.controllers["missile"].on_missile_ready - ) + # Construct the full module path + module_path = f"classes.controllers.{module_name}" - self.event_manager.add_listener( - "play_delay_complete", self.controllers["bomb"].on_play_delay_complete - ) + # Import the module dynamically + module = importlib.import_module(module_path) - self.event_manager.add_listener( - "left_button_pressed", self.controllers["player"].on_move_left - ) - self.event_manager.add_listener( - "left_button_released", self.controllers["player"].on_move_left_exit - ) - self.event_manager.add_listener( - "right_button_pressed", self.controllers["player"].on_move_right - ) - self.event_manager.add_listener( - "right_button_released", self.controllers["player"].on_move_right_exit - ) + # Check if the module defines a controller class and add it to the list + for name, obj in inspect.getmembers(module): + if ( + inspect.isclass(obj) + and issubclass(obj, Controller) + and obj != Controller + ): + # Create an instance of the controller with the config + controller_instance = obj(config) + if not hasattr(controller_instance, "rendering_order"): + controller_instance.rendering_order = 0 - self.event_manager.add_listener( - "fire_button_pressed", self.controllers["missile"].on_fire_pressed - ) + self.controllers.append(controller_instance) - self.event_manager.add_listener( - "fire_button_pressed", self.controllers["mothership"].on_update_shot_counter - ) + self.controllers.sort(key=lambda controller: controller.rendering_order) - self.event_manager.add_listener( - "escape_button_pressed", self.on_escape_button_pressed - ) + def setup_game_events(self): + pass + # self.event_manager.add_listener("swarm_complete", self.on_swarm_complete) def on_escape_button_pressed(self): pygame.quit() @@ -185,8 +86,8 @@ def on_escape_button_pressed(self): def on_swarm_complete(self, data): self.invader_swarm_complete = True - def launch_player_missile(self, player_rect): - self.player_missile = PlayerMissile(player_rect) + # def launch_player_missile(self, player_rect): + # self.player_missile = PlayerMissile(player_rect) def player_missile_remove(self): self.player_missile = None @@ -202,12 +103,13 @@ def update(self, events, dt): # create a new game surface each frame game_surface = pygame.Surface(self.original_screen_size, pygame.SRCALPHA) - for controller in self.controllers.values(): - if hasattr(controller, "update"): - canvas_item = controller.update(events, dt) - # print(canvas_item) - if hasattr(canvas_item, "draw"): - canvas_item.draw(game_surface) + for controller_instance in self.controllers: + # Call the update method if it exists on the controller + if hasattr(controller_instance, "update"): + canvas_item = controller_instance.update(events, dt) + # Check if the controller returns an object with a "draw" method + if canvas_item and hasattr(canvas_item, "draw"): + canvas_item.draw(game_surface) # render the playing surface onto the main window self.window_surface.blit(self.scaled_image, self.top_left) diff --git a/classes/Audio_controller.py b/classes/controllers/Audio_controller.py similarity index 64% rename from classes/Audio_controller.py rename to classes/controllers/Audio_controller.py index 40c00ff..6cea6f8 100644 --- a/classes/Audio_controller.py +++ b/classes/controllers/Audio_controller.py @@ -1,6 +1,5 @@ import pygame -from classes.Controller import Controller -from classes.mothership.Mothership import Mothership +from lib.Controller import Controller pygame.mixer.init() @@ -13,9 +12,16 @@ def __init__(self, config): self.mothership_bonus_sound = pygame.mixer.Sound( audio_config["mothership_bonus"] ) + self.event_manager.add_listener( + "mothership_spawned", self.on_mothership_spawned + ) + self.event_manager.add_listener("mothership_hit", self.on_mothership_bonus) + + self.event_manager.add_listener("mothership_exit", self.on_mothership_exit) def on_mothership_spawned(self, data): - self.mothership_sound.play(-1) + pass + # self.mothership_sound.play(-1) def on_mothership_exit(self, data): self.mothership_sound.fadeout(1000) diff --git a/classes/Baseline_controller.py b/classes/controllers/Baseline_controller.py similarity index 52% rename from classes/Baseline_controller.py rename to classes/controllers/Baseline_controller.py index db3c211..a3fdead 100644 --- a/classes/Baseline_controller.py +++ b/classes/controllers/Baseline_controller.py @@ -1,8 +1,9 @@ import pygame, os +from lib.Controller import Controller -class BaselineController: - def __init__(self): +class BaselineController(Controller): + def __init__(self, config): self.baselineSprite = pygame.sprite.Sprite() self.baselineImage = pygame.image.load( @@ -30,24 +31,24 @@ def get_base_row(self, bomb_collision): # masked_canvas.blit(bomb_collision.image, (bomb_collision.rect.x, bottom_row_y), special_flags=pygame.BLEND_RGBA_MULT) def update(self, events, dt): - if self.get_bombs_callback: - bomb_sprites = self.get_bombs_callback() - if len(bomb_sprites) > 0: - collisions = pygame.sprite.spritecollide( - self.baselineSprite, bomb_sprites, False - ) - if collisions: - for bomb_collision in collisions: - bomb_collision.rect.y = 233 - bomb_collision.active = False - masked_canvas = self.baselineSprite.image.copy() - masked_canvas.blit( - self.baselineImage, - # where to draw on the canvas. here its the x position of the bomb and 0 on the y - (bomb_collision.rect.x, 0), - special_flags=pygame.BLEND_RGBA_MULT, - ) - self.baselineSprite.image = masked_canvas - # bomb_collision.kill() + bomb_callback = self.get_callback("get_bombs") + bomb_sprites = bomb_callback() + if len(bomb_sprites) > 0: + collisions = pygame.sprite.spritecollide( + self.baselineSprite, bomb_sprites, False + ) + if collisions: + for bomb_collision in collisions: + bomb_collision.rect.y = 233 + bomb_collision.active = False + masked_canvas = self.baselineSprite.image.copy() + masked_canvas.blit( + self.baselineImage, + # where to draw on the canvas. here its the x position of the bomb and 0 on the y + (bomb_collision.rect.x, 0), + special_flags=pygame.BLEND_RGBA_MULT, + ) + self.baselineSprite.image = masked_canvas + # bomb_collision.kill() return self diff --git a/classes/bomb/Bomb_controller.py b/classes/controllers/Bomb_controller.py similarity index 76% rename from classes/bomb/Bomb_controller.py rename to classes/controllers/Bomb_controller.py index c9d8302..f7b317e 100644 --- a/classes/bomb/Bomb_controller.py +++ b/classes/controllers/Bomb_controller.py @@ -1,4 +1,4 @@ -from classes.Controller import Controller +from lib.Controller import Controller from classes.bomb.Bomb_factory import BombFactory from classes.bomb.Bomb_container import BombContainer from classes.invader.Invader import Invader @@ -8,6 +8,7 @@ class BombController(Controller): def __init__(self, config): + super().__init__(config) self.counter = 0 self.enabled = False self.max_bombs = 1 @@ -19,18 +20,14 @@ def __init__(self, config): self.explode_bomb_image = pygame.image.load( os.path.join("sprites", "invader_bomb", "bomb_exploding.png") ) + self.event_manager.add_listener( + "play_delay_complete", self.on_play_delay_complete + ) - # callbacks defined in Game_controller - self.get_invaders_callback = None - self.get_player_callback = None + self.register_callback("get_bombs", self.get_bombs) - # callback from game controller - def enable_bombs(self): - self.enabled = True - - # callback from game controller - def disable_bombs(self): - self.enabled = False + # self.get_invaders_callback = self.get_callback("get_invaders") + # self.get_player_callback = self.get_callback("get_player") def get_bombs(self): return self.bomb_container.get_bombs() @@ -40,13 +37,14 @@ def on_play_delay_complete(self, data): self.enabled = True def update(self, events, dt): - if self.get_invaders_callback: - if len(self.get_invaders_callback()) > 0 and self.enabled == True: - # create a new bomb - if len(self.bomb_container.get_bombs()) < self.max_bombs: - bomb = self.create_bomb() - if isinstance(bomb, Bomb): - self.bomb_container.add(bomb) + invader_callback = self.get_callback("get_invaders") + invaders = invader_callback() + if len(invaders) > 0 and self.enabled == True: + # create a new bomb + if len(self.bomb_container.get_bombs()) < self.max_bombs: + bomb = self.create_bomb() + if isinstance(bomb, Bomb): + self.bomb_container.add(bomb) # Update all existing bomb sprites in this container self.counter += 1 @@ -86,7 +84,9 @@ def find_attacking_invader(self, bomb_type): def find_invaders_with_clear_path(self): invaders_with_clear_path = [] - invader_group = self.get_invaders_callback() + invader_callback = self.get_callback("get_invaders") + invader_group = invader_callback() + # find the lowest screen row (initially row 4) of remaining invaders # invaders on this row number won't need a path check max_row = max(invader_group, key=lambda invader: invader.row).row @@ -97,9 +97,6 @@ def find_invaders_with_clear_path(self): # if the invader is on the lowest screen row (highest row number) then don't check any further if invader.row == max_row: invaders_with_clear_path.append(invader) - # invader.image = pygame.image.load( - # "sprites/invader/invader-explode.png" - # ).convert_alpha() continue # else begin inner loop: @@ -113,8 +110,5 @@ def find_invaders_with_clear_path(self): if clear_path: invaders_with_clear_path.append(invader) - # invader.image = pygame.image.load( - # "sprites/invader/invader-explode.png" - # ).convert_alpha() return invaders_with_clear_path diff --git a/classes/Input_controller.py b/classes/controllers/Input_controller.py similarity index 95% rename from classes/Input_controller.py rename to classes/controllers/Input_controller.py index 1c31087..de41cbe 100644 --- a/classes/Input_controller.py +++ b/classes/controllers/Input_controller.py @@ -1,6 +1,5 @@ -import pygame from pygame.locals import * -from classes.Controller import Controller +from lib.Controller import Controller # could be configured with allowed keys diff --git a/classes/invader/Invader_controller.py b/classes/controllers/Invader_controller.py similarity index 74% rename from classes/invader/Invader_controller.py rename to classes/controllers/Invader_controller.py index 1e3c15d..599b685 100644 --- a/classes/invader/Invader_controller.py +++ b/classes/controllers/Invader_controller.py @@ -1,4 +1,4 @@ -from classes.Controller import Controller +from lib.Controller import Controller from classes.invader.Invader_factory import InvaderFactory from classes.invader.Invader_container import InvaderContainer @@ -13,9 +13,20 @@ def __init__(self, config): self.swarm_complete = False self.countdown = 0 - # callbacks defined in Game_controller - self.pause_player_missile_callback = None - self.resume_player_missile_callback = None + self.event_manager.add_listener("invader_hit", self.on_invader_hit) + + self.register_callback( + "get_invaders", lambda: self.invader_container.get_invaders() + ) + + self.register_callback( + "get_invader_count", lambda: len(self.invader_container.get_invaders()) + ) + + self.register_callback( + "get_lowest_invader_y", + lambda: self.invader_container.get_invaders()[0].rect.y, + ) def generate_next_invader(self): try: @@ -26,26 +37,11 @@ def generate_next_invader(self): self.is_moving = True self.event_manager.notify("swarm_complete") - def get_lowest_invader_y(self): - return self.invader_container.get_invaders()[0].rect.y - - def get_invader_count(self): - return len(self.invader_container.get_invaders()) - - def get_invaders(self): - return self.invader_container.get_invaders() - - def stop_movement(self): - self.is_moving = False - - def start_movement(self): - self.is_moving = True - def check_has_landed(): pass def on_invader_hit(self, invader): - self.stop_movement() + self.is_moving = False # # pause invaders 1/4 second (60/15) self.countdown = 15 invader.explode() @@ -53,7 +49,7 @@ def on_invader_hit(self, invader): def release_non_active(self): self.invader_container.remove_inactive() - self.start_movement() + self.is_moving = True self.event_manager.notify("invader_removed") def update(self, events, dt): diff --git a/classes/mothership/Mothership_controller.py b/classes/controllers/Mothership_controller.py similarity index 75% rename from classes/mothership/Mothership_controller.py rename to classes/controllers/Mothership_controller.py index 34a3a48..de791b0 100644 --- a/classes/mothership/Mothership_controller.py +++ b/classes/controllers/Mothership_controller.py @@ -1,5 +1,5 @@ import pygame -from classes.Controller import Controller +from lib.Controller import Controller from classes.mothership.Mothership import Mothership @@ -13,11 +13,11 @@ def __init__(self, config): self.spawned = False self.shot_counter = 0 - # callbacks injected by game_controller - self.get_invader_count_callback = lambda: None - self.get_lowest_invader_y_callback = lambda: None - self.get_missile_callback = lambda: None - self.get_score_text_callback = lambda: None + self.register_callback("mothership_is_exploding", self.mothership_is_exploding) + + self.event_manager.add_listener( + "fire_button_pressed", self.on_update_shot_counter + ) # mothership sprite stored in sprite group self.mothership_group = pygame.sprite.Group() @@ -45,15 +45,16 @@ def update_mothership(self, dt): self.event_manager.notify("mothership_exit") def update_spawn_logic(self): - # if not self.check_invader_criteria(): - # return False + if not self.check_invader_criteria(): + return False self.cycles_lapsed += 1 if self.cycles_lapsed == self.mothership_config["cycles_until_spawn"]: self.spawn() def check_missile_collision(self): - missile = self.get_missile_callback() + missile_callback = self.get_callback("get_player_missile") + missile = missile_callback() mothership = self.mothership_group.sprites() if missile is not None and missile.active: collided = pygame.sprite.spritecollide(missile, mothership, False) @@ -64,8 +65,11 @@ def check_missile_collision(self): def mothership_hit(self): mothership = self.mothership_group.sprites()[0] points = mothership.calculate_points() - score_text_surface = self.get_score_text_callback(str(points)) - mothership.explode(score_text_surface) + + text_surface_callback = self.get_callback("get_score_text") + points_surface = text_surface_callback(str(points)) + + mothership.explode(points_surface) self.event_manager.notify("mothership_hit", points) def reset_spawn_state(self): @@ -77,8 +81,13 @@ def on_update_shot_counter(self, data): self.shot_counter = (self.shot_counter + 1) % 16 def check_invader_criteria(self): - invader_count = self.get_invader_count_callback() - lowest_invader_y = self.get_lowest_invader_y_callback() + invader_count_callback = self.get_callback("get_invader_count") + invader_count = ( + invader_count_callback() + ) # also possible: invader_count = self.get_callback("get_invader_count")() + + lowest_invader_y_callback = self.get_callback("get_lowest_invader_y") + lowest_invader_y = lowest_invader_y_callback() return ( invader_count >= 8 @@ -99,7 +108,7 @@ def get_spawn_position(self): def spawn(self): self.event_manager.notify("mothership_spawned") self.spawned = True - print("Spawning mothership") + # print("Spawning mothership") self.mothership_group.add( Mothership( self.get_spawn_position(), diff --git a/classes/player/Player_controller.py b/classes/controllers/Player_controller.py similarity index 66% rename from classes/player/Player_controller.py rename to classes/controllers/Player_controller.py index 95dcd23..c7b4bc2 100644 --- a/classes/player/Player_controller.py +++ b/classes/controllers/Player_controller.py @@ -1,4 +1,4 @@ -from classes.Controller import Controller +from lib.Controller import Controller from classes.player.Player import Player player_speed = 1 @@ -14,6 +14,18 @@ def __init__(self, config): self.left_key_pressed = False self.right_key_pressed = False + self.register_callback("get_player", self.get_player) + + self.event_manager.add_listener( + "play_delay_complete", self.on_play_delay_complete + ) + self.event_manager.add_listener("left_button_pressed", self.on_move_left) + self.event_manager.add_listener("left_button_released", self.on_move_left_exit) + self.event_manager.add_listener("right_button_pressed", self.on_move_right) + self.event_manager.add_listener( + "right_button_released", self.on_move_right_exit + ) + def on_play_delay_complete(self, data): self.enabled = True diff --git a/classes/player/Player_missile_controller.py b/classes/controllers/Player_missile_controller.py similarity index 75% rename from classes/player/Player_missile_controller.py rename to classes/controllers/Player_missile_controller.py index 8646b6d..352a96e 100644 --- a/classes/player/Player_missile_controller.py +++ b/classes/controllers/Player_missile_controller.py @@ -1,31 +1,38 @@ -from classes.Controller import Controller +from lib.Controller import Controller from classes.player.Player_missile import PlayerMissile import pygame class PlayerMissileController(Controller): - def __init__(self): + def __init__(self, config): super().__init__([]) self.ready_flag = False # there will only ever be one sprite in this group self.missile_group = pygame.sprite.Group() + self.register_callback("get_player_missile", self.get_player_missile) + + self.event_manager.add_listener("invader_removed", self.on_missile_ready) + self.event_manager.add_listener("play_delay_complete", self.on_missile_ready) + self.event_manager.add_listener("fire_button_pressed", self.on_fire_pressed) + # callbacks for interacting with other components - self.get_invaders_callback = lambda: None - self.get_shields_callback = lambda: None - self.get_player_callback = lambda: None - self.mothership_is_exploding = lambda: None + self.get_invaders_callback = self.get_callback("get_invaders") + # self.get_shields_callback = self.get_callback("") + self.get_player_callback = self.get_callback("get_player") + self.mothership_is_exploding = self.get_callback("mothership_is_exploding") def on_missile_ready(self, data): self.ready_flag = True def on_fire_pressed(self, data): + player = self.get_player_callback() if ( not self.missile_group and self.ready_flag and not self.mothership_is_exploding() ): - self.missile_group.add(PlayerMissile(self.get_player_callback().rect)) + self.missile_group.add(PlayerMissile(player.rect)) def check_collisions(self): if self.missile_group: diff --git a/classes/Scoreboard_controller.py b/classes/controllers/Scoreboard_controller.py similarity index 54% rename from classes/Scoreboard_controller.py rename to classes/controllers/Scoreboard_controller.py index 016b218..668827f 100644 --- a/classes/Scoreboard_controller.py +++ b/classes/controllers/Scoreboard_controller.py @@ -1,4 +1,4 @@ -from classes.Controller import Controller +from lib.Controller import Controller class ScoreboardController(Controller): @@ -7,6 +7,11 @@ def __init__(self, config): self.score = 0 self.update_ui_callback = lambda: None + self.register_callback("get_score", self.get_score) + + self.event_manager.add_listener("mothership_hit", self.on_points_awarded) + self.event_manager.add_listener("points_awarded", self.on_points_awarded) + def on_points_awarded(self, points): self.score += points diff --git a/classes/shield/Shield_controller.py b/classes/controllers/Shield_controller.py similarity index 88% rename from classes/shield/Shield_controller.py rename to classes/controllers/Shield_controller.py index 03e60a2..6d233eb 100644 --- a/classes/shield/Shield_controller.py +++ b/classes/controllers/Shield_controller.py @@ -1,4 +1,4 @@ -from classes.Controller import Controller +from lib.Controller import Controller from classes.shield.Shield_factory import ShieldFactory import pygame @@ -6,6 +6,7 @@ class ShieldController(Controller): def __init__(self, config): super().__init__(config) + self.rendering_order = -1 self.shield_container = ShieldFactory(config).create_shields() self.explode_bomb_image = pygame.image.load( "sprites/invader_bomb/bomb_exploding.png" @@ -14,15 +15,8 @@ def __init__(self, config): "sprites/player/player-shot-double-height.png" ) - # self.bomb_stem_image = pygame.image.load( - # "sprites/invader_bomb/explode-stem.png" - # ) - - # callbacks defined in Game_controller - # if not injected they are still callable - self.get_bombs_callback = lambda: None - self.get_invaders_callback = lambda: None - self.get_missile_callback = lambda: None + # self.get_invaders_callback = self.get_callback("get_invaders") + # self.get_missile_callback = self.get_callback("get_player_missile") def update(self, events, dt): self.check_bomb_collisions() @@ -31,7 +25,8 @@ def update(self, events, dt): return self.shield_container def check_missile_collision(self): - missile = self.get_missile_callback() + missile_callback = self.get_callback("get_player_missile") + missile = missile_callback() if missile is not None and missile.active: _missile = pygame.sprite.Sprite() # Create an instance of the Sprite class @@ -52,7 +47,8 @@ def check_missile_collision(self): # self.event_manager.notify("missile_collision", shield_sprite) def check_invader_collision(self): - invaders = self.get_invaders_callback() + invader_callback = self.get_callback("get_invaders") + invaders = invader_callback() if invaders: collisions = pygame.sprite.groupcollide( self.shield_container, invaders, False, False @@ -62,7 +58,9 @@ def check_invader_collision(self): shield_sprite.invader_damage(invader) def check_bomb_collisions(self): - bomb_sprites = self.get_bombs_callback() + bomb_callback = self.get_callback("get_bombs") + bomb_sprites = bomb_callback() + if bomb_sprites is not None: for shield_sprite in self.shield_container: for bomb_sprite in bomb_sprites: diff --git a/classes/UI_controller.py b/classes/controllers/UI_controller.py similarity index 92% rename from classes/UI_controller.py rename to classes/controllers/UI_controller.py index 490c786..147c7a6 100644 --- a/classes/UI_controller.py +++ b/classes/controllers/UI_controller.py @@ -1,5 +1,5 @@ import pygame -from classes.Controller import Controller +from lib.Controller import Controller class UIController(Controller): @@ -14,7 +14,9 @@ def __init__(self, config): (self.canvas_width, self.canvas_height), pygame.SRCALPHA ) - self.get_score_callback = lambda: None + self.register_callback("get_score_text", self.create_text_surface) + + self.get_score_callback = self.get_callback("get_score") def draw(self, surface): surface.blit(self.canvas, (0, 0)) # blit the canvas onto the game surface diff --git a/classes/mothership/Mothership.py b/classes/mothership/Mothership.py index 1daf561..7fefea7 100644 --- a/classes/mothership/Mothership.py +++ b/classes/mothership/Mothership.py @@ -67,7 +67,5 @@ def modify_pixel_colors(self): and pixel_color[1] == 255 and pixel_color[2] == 255 ): - # print("all white") - # print(pixel_color) pixel_color.r, pixel_color.g, pixel_color.b = red self.image.set_at((x, y), pixel_color) diff --git a/game.py b/game.py index e15b6bb..de32ce0 100644 --- a/game.py +++ b/game.py @@ -10,6 +10,7 @@ game_controller = GameController(config) +game_controller.load_controllers(config) running = True while running: diff --git a/lib/Controller.py b/lib/Controller.py new file mode 100644 index 0000000..ef15094 --- /dev/null +++ b/lib/Controller.py @@ -0,0 +1,43 @@ +import pygame +from lib.Event_manager import EventManager + +import importlib +import os +import inspect + + +class Controller: + def __init__(self, config): + self.event_manager = EventManager.get_instance() + self.config = config + + # Class-level dictionary to store callbacks + callbacks = {} + + # Class-level dictionary to store callback names (labels) + callback_names = {} + + @classmethod + def register_callback(cls, key, callback, name=None): + cls.callbacks[key] = callback + + # Store the callback name if provided + if name: + cls.callback_names[key] = name + + @classmethod + def get_callback(cls, key): + return cls.callbacks.get(key) + + @classmethod + def debug_callbacks(cls): + print("Callbacks:") + for key, callback in cls.callbacks.items(): + # Get the callback name from the dictionary, or use the key as a fallback + name = cls.callback_names.get(key, key) + print( + f"{name}: {callback.__name__ if hasattr(callback, '__name__') else callback}" + ) + + +pygame.quit() diff --git a/classes/Event_manager.py b/lib/Event_manager.py similarity index 100% rename from classes/Event_manager.py rename to lib/Event_manager.py diff --git a/test.py b/test.py new file mode 100644 index 0000000..12dc04c --- /dev/null +++ b/test.py @@ -0,0 +1,125 @@ +class MyController: + callbacks = {} # Class-level dictionary to store callbacks + + def __init__(self): + pass + + @classmethod + def register_callback(cls, key, callback): + cls.callbacks[key] = callback + + @classmethod + def get_callback(cls, key): + return cls.callbacks.get(key) + + +class ShieldController(MyController): + def __init__(self): + super().__init__() + self.register_callbacks() + + def get_shields(self): + return ["Shield1", "Shield2"] + + def get_shield_count(self): + return 2 + + def register_callbacks(self): + self.register_callback("get_shields", self.get_shields) + self.register_callback("get_shield_count", self.get_shield_count) + + self.get_invader_callback = self.get_callback("get_invaders") + self.get_bombs_callback = self.get_callback("get_bombs") + + def activate_callback(self): + print("inside shield controller") + bomb_callback = self.get_callback("get_bombs") + invader_callback = self.get_callback("get_invaders") + + invaders = invader_callback() + bombs = bomb_callback() + print(f"Invaders: {invaders}") + print(f"Bombs: {bombs}") + print("-----------") + + +class BombController(MyController): + def __init__(self): + super().__init__() + self.register_callbacks() + + def get_bombs(self): + return ["Bomb1", "Bomb2"] + + def get_bomb_count(self): + return 2 + + def register_callbacks(self): + # Register callbacks specific to BombController + self.register_callback("get_bombs", self.get_bombs) + self.register_callback("get_bomb_count", self.get_bomb_count) + + self.get_invaders_callback = self.get_callback("get_invaders") + self.get_shields_callback = self.get_callback("get_shields") + + def activate_callback(self): + print("inside bomb controller") + invader_callback = self.get_callback("get_invaders") + invaders = invader_callback() + print(f"Invaders: {invaders}") + + shield_callback = self.get_callback("get_shields") + shields = shield_callback() + print(f"Shields: {shields}") + print("-------------") + + +class InvaderController(MyController): + def __init__(self): + super().__init__() + self.register_callbacks() + + def register_callbacks(self): + # Register callbacks specific to BombController + self.register_callback("get_invaders", self.get_invaders) + self.register_callback("get_invader_count", self.get_invader_count) + self.get_bombs_callback = self.get_callback("get_bombs") + + def get_invaders(self): + return ["Invader1", "Invader2", "Invader3"] + + def get_invader_count(self): + return 3 + + def activate_callback(self): + print("inside invader controller") + bomb_callback = self.get_callback("get_bombs") + bombs = bomb_callback() + print(f"Bombs: {bombs}") + print("-----------") + + +# Create an instance of BombController +invader_controller = InvaderController() +bomb_controller = BombController() +shield_controller = ShieldController() + +bomb_controller.activate_callback() +invader_controller.activate_callback() +shield_controller.activate_callback() + +# Retrieve and use the "get_invaders" callback +# get_invaders_callback = invader_controller.get_callback("get_invaders") +# get_bombs_callback = bomb_controller.get_callback("get_bombs") + +# if get_invaders_callback: +# invaders = get_invaders_callback() +# print(f"Invaders: {invaders}") +# else: +# print("Callback not found") + +# if get_bombs_callback: +# bombs = get_bombs_callback() +# print(f"Bombs: {bombs}") +# else: +# print("Callback not found") From e79ce427af23c0244eb821102c77aa569a6192cd Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Sun, 10 Sep 2023 16:46:57 +0100 Subject: [PATCH 2/6] individual config files --- classes/Game_controller.py | 46 ++----- classes/bomb/Bomb_factory.py | 18 +-- classes/config/Audio_config.py | 9 ++ classes/config/Base_config.py | 14 ++ classes/config/Bomb_config.py | 32 +++++ classes/config/Game_config.py | 12 ++ classes/config/Invader_config.py | 33 +++++ classes/config/Mothership_config.py | 35 +++++ classes/config/Shield_config.py | 9 ++ classes/config/UI_config.py | 55 ++++++++ classes/controllers/Audio_controller.py | 14 +- classes/controllers/Baseline_controller.py | 8 +- classes/controllers/Bomb_controller.py | 7 +- classes/controllers/Input_controller.py | 4 +- classes/controllers/Invader_controller.py | 23 ++-- classes/controllers/Mothership_controller.py | 35 ++--- classes/controllers/Player_controller.py | 10 +- .../controllers/Player_missile_controller.py | 4 +- classes/controllers/Scoreboard_controller.py | 4 +- classes/controllers/Shield_controller.py | 15 +-- classes/controllers/UI_controller.py | 23 ++-- classes/invader/Invader.py | 7 +- classes/invader/Invader_container.py | 27 ++-- classes/invader/Invader_factory.py | 89 ++++++++----- classes/mothership/Mothership.py | 5 +- classes/shield/Shield_factory.py | 12 +- game.py | 18 +-- lib/Controller.py | 3 +- test.py | 125 ------------------ 29 files changed, 384 insertions(+), 312 deletions(-) create mode 100644 classes/config/Audio_config.py create mode 100644 classes/config/Base_config.py create mode 100644 classes/config/Bomb_config.py create mode 100644 classes/config/Game_config.py create mode 100644 classes/config/Invader_config.py create mode 100644 classes/config/Mothership_config.py create mode 100644 classes/config/Shield_config.py create mode 100644 classes/config/UI_config.py delete mode 100644 test.py diff --git a/classes/Game_controller.py b/classes/Game_controller.py index 631cfda..a23df09 100644 --- a/classes/Game_controller.py +++ b/classes/Game_controller.py @@ -2,20 +2,16 @@ import inspect import os import pygame -import sys from lib.Controller import Controller +from classes.config.Game_config import GameConfig class GameController(Controller): - def __init__(self, config): - super().__init__(config) - self.bombs_enabled = False + def __init__(self): + super().__init__() + config = GameConfig() self.play_delay_count = 120 - self.player_on_screen = False - - self.player_missile = None - # self.invader_swarm_complete = False self.top_left = config.get("top_left") self.original_screen_size = config.get("original_screen_size") self.larger_screen_size = config.get("larger_screen_size") @@ -25,14 +21,11 @@ def __init__(self, config): self.scaled_image = pygame.transform.scale(_bg_image, self.larger_screen_size) # add , pygame.FULLSCREEN to run without border self.window_surface = pygame.display.set_mode(self.larger_screen_size) - self.max_fps = config.get("max_fps") self.event_manager.add_listener( "escape_button_pressed", self.on_escape_button_pressed ) - self.setup_game_events() - def debug_controllers(self): print("Ordered Controllers:") for controller in self.controllers: @@ -40,7 +33,7 @@ def debug_controllers(self): f"{controller.__class__.__name__} - Rendering Order: {controller.rendering_order}" ) - def load_controllers(self, config): + def load_controllers(self): # Initialize an empty list to store the imported controllers self.controllers = [] @@ -49,7 +42,7 @@ def load_controllers(self, config): # Loop through files in the controllers directory for filename in os.listdir(controllers_directory): - if filename.endswith("_controller.py"): + if filename.endswith("_controller.py") and filename != "game_controller.py": # Extract the module name without the extension module_name = filename[:-3] @@ -66,8 +59,8 @@ def load_controllers(self, config): and issubclass(obj, Controller) and obj != Controller ): - # Create an instance of the controller with the config - controller_instance = obj(config) + # Create an instance of the controller + controller_instance = obj() if not hasattr(controller_instance, "rendering_order"): controller_instance.rendering_order = 0 @@ -75,26 +68,12 @@ def load_controllers(self, config): self.controllers.sort(key=lambda controller: controller.rendering_order) - def setup_game_events(self): - pass - # self.event_manager.add_listener("swarm_complete", self.on_swarm_complete) - - def on_escape_button_pressed(self): + def on_escape_button_pressed(self, data): pygame.quit() - sys.exit() - - def on_swarm_complete(self, data): - self.invader_swarm_complete = True - - # def launch_player_missile(self, player_rect): - # self.player_missile = PlayerMissile(player_rect) - - def player_missile_remove(self): - self.player_missile = None def update(self, events, dt): - clock = pygame.time.Clock() - + # sys.exit() + # return if self.play_delay_count > 0: self.play_delay_count -= 1 if self.play_delay_count <= 0: @@ -118,6 +97,3 @@ def update(self, events, dt): pygame.transform.scale(game_surface, self.larger_screen_size), self.top_left, ) - - pygame.display.flip() - clock.tick(self.max_fps) diff --git a/classes/bomb/Bomb_factory.py b/classes/bomb/Bomb_factory.py index a530d18..aa3082f 100644 --- a/classes/bomb/Bomb_factory.py +++ b/classes/bomb/Bomb_factory.py @@ -1,29 +1,29 @@ import pygame +from classes.config.Bomb_config import BombConfig from classes.bomb.Bomb import Bomb class BombFactory: - def __init__(self, config): - self.config = config - self.bomb_frames = config.get("bombs")["images"] + def __init__(self): + config = BombConfig() + self.bomb_frames = config.get("images") # Load bomb images using config into array for bomb_type, frames in self.bomb_frames.items(): loaded_frames = [] for frame in frames: - image = pygame.image.load(self.config.get_file_path(frame)) + image = pygame.image.load(config.get_file_path(frame)) loaded_frames.append(image) self.bomb_frames[bomb_type] = loaded_frames - self.explode_frame = pygame.image.load( - "sprites/invader_bomb/bomb_exploding.png" + self.exploding_frame = pygame.image.load( + config.get("exploding_image") ).convert_alpha() def create_bomb(self, invader, bomb_type): - x = invader.rect.x + 7 - y = invader.rect.y + 8 + x, y = invader.bomb_launch_position() bomb_sprite = Bomb( - x, y, self.bomb_frames[bomb_type], self.explode_frame, bomb_type + x, y, self.bomb_frames[bomb_type], self.exploding_frame, bomb_type ) return bomb_sprite diff --git a/classes/config/Audio_config.py b/classes/config/Audio_config.py new file mode 100644 index 0000000..e675d0b --- /dev/null +++ b/classes/config/Audio_config.py @@ -0,0 +1,9 @@ +import os +from classes.config.Base_config import BaseConfig + + +class AudioConfig(BaseConfig): + config_values = { + "mothership": "sounds/mothership.wav", + "mothership_bonus": "sounds/mothership_bonus.wav", + } diff --git a/classes/config/Base_config.py b/classes/config/Base_config.py new file mode 100644 index 0000000..e702e08 --- /dev/null +++ b/classes/config/Base_config.py @@ -0,0 +1,14 @@ +import os + + +class BaseConfig: + @classmethod + def get(cls, key): + return cls.config_values.get(key) + + @classmethod + def get_file_path(cls, key): + base_directory = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + parent_directory = os.path.dirname(base_directory) + file_path = os.path.join(parent_directory, key) + return file_path diff --git a/classes/config/Bomb_config.py b/classes/config/Bomb_config.py new file mode 100644 index 0000000..dc5329b --- /dev/null +++ b/classes/config/Bomb_config.py @@ -0,0 +1,32 @@ +import os +from classes.config.Base_config import BaseConfig + + +class BombConfig(BaseConfig): + config_values = { + "exploding_image": os.path.join( + "sprites", "invader_bomb", "bomb_exploding.png" + ), + "images": { + "plunger": [ + os.path.join("sprites", "invader_bomb", "plunger-frame1.png"), + os.path.join("sprites", "invader_bomb", "plunger-frame2.png"), + os.path.join("sprites", "invader_bomb", "plunger-frame3.png"), + os.path.join("sprites", "invader_bomb", "plunger-frame4.png"), + ], + "squiggly": [ + os.path.join("sprites", "invader_bomb", "squiggly-frame1.png"), + os.path.join("sprites", "invader_bomb", "squiggly-frame2.png"), + os.path.join("sprites", "invader_bomb", "squiggly-frame3.png"), + os.path.join("sprites", "invader_bomb", "squiggly-frame4.png"), + ], + "rolling": [ + os.path.join("sprites", "invader_bomb", "rolling-frame1.png"), + os.path.join("sprites", "invader_bomb", "rolling-frame2.png"), + os.path.join("sprites", "invader_bomb", "rolling-frame3.png"), + os.path.join("sprites", "invader_bomb", "rolling-frame4.png"), + ], + }, + "max_bombs": 1, + "grace_period": 60, + } diff --git a/classes/config/Game_config.py b/classes/config/Game_config.py new file mode 100644 index 0000000..1a448bd --- /dev/null +++ b/classes/config/Game_config.py @@ -0,0 +1,12 @@ +import os +from classes.config.Base_config import BaseConfig + + +class GameConfig(BaseConfig): + config_values = { + "original_screen_size": (224, 256), + "larger_screen_size": (224 * 4, 256 * 4), + "bg_image_path": os.path.join("images", "invaders_moon_bg.png"), + "max_fps": 60, + "top_left": (0, 0), + } diff --git a/classes/config/Invader_config.py b/classes/config/Invader_config.py new file mode 100644 index 0000000..9c96b21 --- /dev/null +++ b/classes/config/Invader_config.py @@ -0,0 +1,33 @@ +import os +from classes.config.Base_config import BaseConfig + + +class InvaderConfig(BaseConfig): + config_values = { + "spawn_rows": [120, 144, 160, 168, 168, 168, 176, 176, 176], + "points": [30, 20, 20, 10, 10], + "cols": 11, + "rows": 5, + "x_position_start": 16, + "x_repeat_offset": 16, + "y_repeat_offset": 17, + "screen_bottom_limit": 218, + "screen_left_limit": 25, + "screen_right_limit": 200, + "horizontal_move": 2, + "vertical_move": 8, + "images": { + "small": [ + os.path.join("sprites", "invader", "invader-small-frame1.png"), + os.path.join("sprites", "invader", "invader-small-frame2.png"), + ], + "mid": [ + os.path.join("sprites", "invader", "invader-mid-frame1.png"), + os.path.join("sprites", "invader", "invader-mid-frame2.png"), + ], + "large": [ + os.path.join("sprites", "invader", "invader-large-frame1.png"), + os.path.join("sprites", "invader", "invader-large-frame2.png"), + ], + }, + } diff --git a/classes/config/Mothership_config.py b/classes/config/Mothership_config.py new file mode 100644 index 0000000..fc2b4d3 --- /dev/null +++ b/classes/config/Mothership_config.py @@ -0,0 +1,35 @@ +import os +from classes.config.Base_config import BaseConfig + + +class MothershipConfig(BaseConfig): + config_values = { + "image_frame": os.path.join("sprites", "mothership", "mothership.png"), + "explode_frame": os.path.join( + "sprites", "mothership", "mothership-exploding.png" + ), + # "cycles_with_explosion_frame": 60, + # "cycles_with_bonus_text": 60, + "cycles_until_spawn": 200, + "qualifying_invader_y_position": 36, + "spawn_left_position": (0, 48), + "spawn_right_position": (224, 48), + "points_table": [ + 50, + 50, + 50, + 50, + 50, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 150, + 150, + 300, + ], + } diff --git a/classes/config/Shield_config.py b/classes/config/Shield_config.py new file mode 100644 index 0000000..1e4e383 --- /dev/null +++ b/classes/config/Shield_config.py @@ -0,0 +1,9 @@ +import os +from classes.config.Base_config import BaseConfig + + +class ShieldConfig(BaseConfig): + config_values = { + "positions": [(29, 191), (75, 191), (127, 191), (173, 191)], + "image": os.path.join("sprites", "player", "player-shield.png"), + } diff --git a/classes/config/UI_config.py b/classes/config/UI_config.py new file mode 100644 index 0000000..085b88e --- /dev/null +++ b/classes/config/UI_config.py @@ -0,0 +1,55 @@ +from classes.config.Base_config import BaseConfig + + +class UIConfig(BaseConfig): + config_values = { + "score_label_text": "SCORE", + "score_label_position": (60, 16), + "score_value_position": (60, 32), + "hiscore_label_text": "HI-SCORE", + "hiscore_label_position": (118, 16), + "hiscore_value_position": (118, 32), + "font_spritesheet_offsets": { + "A": [1, 1], + "B": [11, 1], + "C": [21, 1], + "D": [31, 1], + "E": [41, 1], + "F": [51, 1], + "G": [61, 1], + "H": [71, 1], + "I": [1, 11], + "J": [11, 11], + "K": [21, 11], + "L": [31, 11], + "M": [41, 11], + "N": [51, 11], + "O": [61, 11], + "P": [71, 11], + "Q": [1, 21], + "R": [11, 21], + "S": [21, 21], + "T": [31, 21], + "U": [41, 21], + "W": [51, 21], + "X": [61, 21], + "Y": [1, 31], + "Z": [11, 31], + "0": [21, 31], + "1": [31, 31], + "2": [41, 31], + "3": [51, 31], + "4": [61, 31], + "5": [71, 31], + "6": [1, 41], + "7": [11, 41], + "8": [21, 41], + "9": [31, 41], + "<": [41, 41], + ">": [51, 41], + "=": [61, 41], + "*": [71, 41], + "?": [1, 51], + "-": [11, 51], + }, + } diff --git a/classes/controllers/Audio_controller.py b/classes/controllers/Audio_controller.py index 6cea6f8..d9b3b34 100644 --- a/classes/controllers/Audio_controller.py +++ b/classes/controllers/Audio_controller.py @@ -1,17 +1,17 @@ import pygame from lib.Controller import Controller +from classes.config.Audio_config import AudioConfig pygame.mixer.init() class AudioController(Controller): - def __init__(self, config): - super().__init__(config) - audio_config = config.get("audio") - self.mothership_sound = pygame.mixer.Sound(audio_config["mothership"]) - self.mothership_bonus_sound = pygame.mixer.Sound( - audio_config["mothership_bonus"] - ) + def __init__(self): + super().__init__() + config = AudioConfig() + self.mothership_sound = pygame.mixer.Sound(config.get("mothership")) + + self.mothership_bonus_sound = pygame.mixer.Sound(config.get("mothership_bonus")) self.event_manager.add_listener( "mothership_spawned", self.on_mothership_spawned ) diff --git a/classes/controllers/Baseline_controller.py b/classes/controllers/Baseline_controller.py index a3fdead..127b54b 100644 --- a/classes/controllers/Baseline_controller.py +++ b/classes/controllers/Baseline_controller.py @@ -3,7 +3,7 @@ class BaselineController(Controller): - def __init__(self, config): + def __init__(self): self.baselineSprite = pygame.sprite.Sprite() self.baselineImage = pygame.image.load( @@ -19,17 +19,11 @@ def __init__(self, config): self.baselineSprite.rect = self.baselineSprite.image.get_rect() self.baselineSprite.rect.x = 0 self.baselineSprite.rect.y = 240 - # self.collision_check = pygame.sprite.spritecollide - self.get_bombs_callback = None def draw(self, surface): # draw the baselineSprite onto the specified surface surface.blit(self.baselineSprite.image, self.baselineSprite.rect.topleft) - def get_base_row(self, bomb_collision): - bomb_collision.rect.height - 1 - # masked_canvas.blit(bomb_collision.image, (bomb_collision.rect.x, bottom_row_y), special_flags=pygame.BLEND_RGBA_MULT) - def update(self, events, dt): bomb_callback = self.get_callback("get_bombs") bomb_sprites = bomb_callback() diff --git a/classes/controllers/Bomb_controller.py b/classes/controllers/Bomb_controller.py index f7b317e..15bb126 100644 --- a/classes/controllers/Bomb_controller.py +++ b/classes/controllers/Bomb_controller.py @@ -1,4 +1,5 @@ from lib.Controller import Controller + from classes.bomb.Bomb_factory import BombFactory from classes.bomb.Bomb_container import BombContainer from classes.invader.Invader import Invader @@ -7,15 +8,15 @@ class BombController(Controller): - def __init__(self, config): - super().__init__(config) + def __init__(self): + super().__init__() self.counter = 0 self.enabled = False self.max_bombs = 1 self.grace_period = 60 self.bomb_types = ["plunger", "squiggly", "rolling"] - self.bomb_factory = BombFactory(config) + self.bomb_factory = BombFactory() self.bomb_container = BombContainer() self.explode_bomb_image = pygame.image.load( os.path.join("sprites", "invader_bomb", "bomb_exploding.png") diff --git a/classes/controllers/Input_controller.py b/classes/controllers/Input_controller.py index de41cbe..aaa1158 100644 --- a/classes/controllers/Input_controller.py +++ b/classes/controllers/Input_controller.py @@ -7,8 +7,8 @@ # maybe the init method could have a mode which would define the keys it tracks # and it could be switched as well as set during config class InputController(Controller): - def __init__(self, config): - super().__init__(config) + def __init__(self): + super().__init__() def update(self, events, dt): for event in events: diff --git a/classes/controllers/Invader_controller.py b/classes/controllers/Invader_controller.py index 599b685..1b25871 100644 --- a/classes/controllers/Invader_controller.py +++ b/classes/controllers/Invader_controller.py @@ -4,11 +4,12 @@ class InvaderController(Controller): - def __init__(self, config): - super().__init__(config) - invader_factory = InvaderFactory(config) + def __init__(self): + super().__init__() + + invader_factory = InvaderFactory() self.invader_generator = invader_factory.create_invader_swarm() - self.invader_container = InvaderContainer(config) + self.invader_container = InvaderContainer() self.is_moving = False self.swarm_complete = False self.countdown = 0 @@ -28,6 +29,13 @@ def __init__(self, config): lambda: self.invader_container.get_invaders()[0].rect.y, ) + def on_invader_hit(self, invader): + self.is_moving = False + # # pause invaders 1/4 second (60/15) + self.countdown = 15 + invader.explode() + self.event_manager.notify("points_awarded", invader.points) + def generate_next_invader(self): try: self.invader_container.add_invader(next(self.invader_generator)) @@ -40,13 +48,6 @@ def generate_next_invader(self): def check_has_landed(): pass - def on_invader_hit(self, invader): - self.is_moving = False - # # pause invaders 1/4 second (60/15) - self.countdown = 15 - invader.explode() - self.event_manager.notify("points_awarded", invader.points) - def release_non_active(self): self.invader_container.remove_inactive() self.is_moving = True diff --git a/classes/controllers/Mothership_controller.py b/classes/controllers/Mothership_controller.py index de791b0..8f8b693 100644 --- a/classes/controllers/Mothership_controller.py +++ b/classes/controllers/Mothership_controller.py @@ -1,12 +1,20 @@ import pygame from lib.Controller import Controller from classes.mothership.Mothership import Mothership +from classes.config.Mothership_config import MothershipConfig class MothershipController(Controller): - def __init__(self, config): - super().__init__(config) - self.mothership_config = config.get("mothership") + def __init__(self): + super().__init__() + self.config = MothershipConfig() + self.cycles_until_spawn = self.config.get("cycles_until_spawn") + self.spawn_right_position = self.config.get("spawn_right_position") + self.spawn_left_position = self.config.get("spawn_left_position") + self.qualifying_invader_y_position = self.config.get( + "qualifying_invader_y_position" + ) + self.points_table = self.config.get("points_table") # set-up non-config related variables self.cycles_lapsed = 0 @@ -49,7 +57,7 @@ def update_spawn_logic(self): return False self.cycles_lapsed += 1 - if self.cycles_lapsed == self.mothership_config["cycles_until_spawn"]: + if self.cycles_lapsed == self.cycles_until_spawn: self.spawn() def check_missile_collision(self): @@ -81,18 +89,12 @@ def on_update_shot_counter(self, data): self.shot_counter = (self.shot_counter + 1) % 16 def check_invader_criteria(self): - invader_count_callback = self.get_callback("get_invader_count") - invader_count = ( - invader_count_callback() - ) # also possible: invader_count = self.get_callback("get_invader_count")() - - lowest_invader_y_callback = self.get_callback("get_lowest_invader_y") - lowest_invader_y = lowest_invader_y_callback() + invader_count = self.get_callback("get_invader_count")() + lowest_invader_y = self.get_callback("get_lowest_invader_y")() return ( invader_count >= 8 - and lowest_invader_y - >= self.mothership_config["qualifying_invader_y_position"] + and lowest_invader_y >= self.qualifying_invader_y_position ) def get_spawn_direction(self): @@ -100,19 +102,18 @@ def get_spawn_direction(self): def get_spawn_position(self): return ( - self.mothership_config["spawn_right_position"] + self.spawn_right_position if self.shot_counter % 2 == 1 - else self.mothership_config["spawn_left_position"] + else self.spawn_left_position ) def spawn(self): self.event_manager.notify("mothership_spawned") self.spawned = True - # print("Spawning mothership") self.mothership_group.add( Mothership( self.get_spawn_position(), self.get_spawn_direction(), - self.mothership_config["points_table"], + self.points_table, ) ) diff --git a/classes/controllers/Player_controller.py b/classes/controllers/Player_controller.py index c7b4bc2..294f988 100644 --- a/classes/controllers/Player_controller.py +++ b/classes/controllers/Player_controller.py @@ -5,8 +5,8 @@ class PlayerController(Controller): - def __init__(self, config): - super().__init__(config) + def __init__(self): + super().__init__() self.can_launch_missile = True self.enabled = False self.player = Player() @@ -44,9 +44,11 @@ def on_move_right(self, data): def update(self, events, dt): if self.enabled: if self.left_key_pressed: - self.player.rect.x -= player_speed * dt + # self.player.rect.x -= player_speed * dt + self.player.rect.x -= player_speed elif self.right_key_pressed: - self.player.rect.x += player_speed * dt + # self.player.rect.x += player_speed * dt + self.player.rect.x += player_speed return self.player def clamp(value, min_value, max_value): diff --git a/classes/controllers/Player_missile_controller.py b/classes/controllers/Player_missile_controller.py index 352a96e..a65d38e 100644 --- a/classes/controllers/Player_missile_controller.py +++ b/classes/controllers/Player_missile_controller.py @@ -4,8 +4,8 @@ class PlayerMissileController(Controller): - def __init__(self, config): - super().__init__([]) + def __init__(self): + super().__init__() self.ready_flag = False # there will only ever be one sprite in this group self.missile_group = pygame.sprite.Group() diff --git a/classes/controllers/Scoreboard_controller.py b/classes/controllers/Scoreboard_controller.py index 668827f..002dffc 100644 --- a/classes/controllers/Scoreboard_controller.py +++ b/classes/controllers/Scoreboard_controller.py @@ -2,8 +2,8 @@ class ScoreboardController(Controller): - def __init__(self, config): - super().__init__(config) + def __init__(self): + super().__init__() self.score = 0 self.update_ui_callback = lambda: None diff --git a/classes/controllers/Shield_controller.py b/classes/controllers/Shield_controller.py index 6d233eb..a72dc4a 100644 --- a/classes/controllers/Shield_controller.py +++ b/classes/controllers/Shield_controller.py @@ -4,20 +4,17 @@ class ShieldController(Controller): - def __init__(self, config): - super().__init__(config) + def __init__(self): + super().__init__() + + shield_factory = ShieldFactory() self.rendering_order = -1 - self.shield_container = ShieldFactory(config).create_shields() - self.explode_bomb_image = pygame.image.load( - "sprites/invader_bomb/bomb_exploding.png" - ) + self.shield_container = shield_factory.create_shields() + self.missile_image_2x = pygame.image.load( "sprites/player/player-shot-double-height.png" ) - # self.get_invaders_callback = self.get_callback("get_invaders") - # self.get_missile_callback = self.get_callback("get_player_missile") - def update(self, events, dt): self.check_bomb_collisions() self.check_invader_collision() diff --git a/classes/controllers/UI_controller.py b/classes/controllers/UI_controller.py index 147c7a6..8938e23 100644 --- a/classes/controllers/UI_controller.py +++ b/classes/controllers/UI_controller.py @@ -1,12 +1,13 @@ import pygame from lib.Controller import Controller +from classes.config.UI_config import UIConfig class UIController(Controller): - def __init__(self, config): - super().__init__(config) - self.font_config = config.get("font_spritesheet_offsets") - self.ui_config = config.get("ui") + def __init__(self): + super().__init__() + self.config = UIConfig() + self.font_config = self.config.get("font_spritesheet_offsets") self.spritesheet = pygame.image.load("images/font_spritesheet.png") self.canvas_width = 224 self.canvas_height = 256 @@ -15,7 +16,6 @@ def __init__(self, config): ) self.register_callback("get_score_text", self.create_text_surface) - self.get_score_callback = self.get_callback("get_score") def draw(self, surface): @@ -29,23 +29,23 @@ def update(self, events, dt): # position SCORE text at position in config self.canvas.blit( - self.create_text_surface(self.ui_config["score_label_text"]), - self.ui_config["score_label_position"], + self.create_text_surface(self.config.get("score_label_text")), + self.config.get("score_label_position"), ) # position SCORE value at position in config text_surface = self.create_text_surface(score) - self.canvas.blit(text_surface, self.ui_config["score_value_position"]) + self.canvas.blit(text_surface, self.config.get("score_value_position")) # position HI-SCORE text at position in config self.canvas.blit( - self.create_text_surface(self.ui_config["hiscore_label_text"]), - self.ui_config["hiscore_label_position"], + self.create_text_surface(self.config.get("hiscore_label_text")), + self.config.get("hiscore_label_position"), ) # position HI-SCORE value at position in config text_surface = self.create_text_surface("00000") - self.canvas.blit(text_surface, self.ui_config["hiscore_value_position"]) + self.canvas.blit(text_surface, self.config.get("hiscore_value_position")) return self @@ -55,6 +55,7 @@ def create_text_surface(self, text): text_surface = pygame.Surface((surface_width, surface_height), pygame.SRCALPHA) text_surface.fill((0, 0, 0, 0)) + # font_spritesheet_offsets = self.font_config.get("font_spritesheet_offsets") for idx, letter in enumerate(text): if letter in self.font_config: letter_x, letter_y = self.font_config[letter] diff --git a/classes/invader/Invader.py b/classes/invader/Invader.py index 1cc6e51..66de452 100644 --- a/classes/invader/Invader.py +++ b/classes/invader/Invader.py @@ -13,10 +13,6 @@ def __init__(self, x, y, active, column, row, image_frames, index, points): self.active = active self.points = points - self.empty_frame = pygame.image.load( - "sprites/invader/invader-empty-frame.png" - ).convert_alpha() - self.explode_frame = pygame.image.load( "sprites/invader/invader-explode.png" ).convert_alpha() @@ -42,6 +38,9 @@ def modify_pixel_colors(self): frame.set_at((x, y), pixel_color) + def bomb_launch_position(self): + return (self.rect.x + 7, self.rect.y + 8) + def explode(self): self.image = self.explode_frame self.active = False diff --git a/classes/invader/Invader_container.py b/classes/invader/Invader_container.py index 40009b5..d5504d5 100644 --- a/classes/invader/Invader_container.py +++ b/classes/invader/Invader_container.py @@ -1,18 +1,21 @@ import pygame from classes.invader.Invader_factory import InvaderFactory +from classes.config.Invader_config import InvaderConfig class InvaderContainer(pygame.sprite.Group): - def __init__(self, config): + def __init__(self): super().__init__() + + config = InvaderConfig() # copy the config values - self.invader_direction = config.get("invaders")["horizontal_move"] - self.invader_down_direction = config.get("invaders")["vertical_move"] - self.screen_left_limit = config.get("invaders")["screen_left_limit"] - self.screen_right_limit = config.get("invaders")["screen_right_limit"] - self.screen_bottom_limit = config.get("invaders")["screen_bottom_limit"] + self.invader_direction = config.get("horizontal_move") + self.invader_down_direction = config.get("vertical_move") + self.screen_left_limit = config.get("screen_left_limit") + self.screen_right_limit = config.get("screen_right_limit") + self.screen_bottom_limit = config.get("screen_bottom_limit") # when an invader reaches the edge of the screen this flag is set - self.invader_move_down = False + self.invaders_moving_down = False # used to track which invader in the group is next to move self.current_invader_index = 0 # flag used when invaders reach bottom of screen - game over @@ -28,8 +31,8 @@ def update(self): # remember last invader moves differently def handle_invader_movement(self): invader = self.get_invader_at_current_index() - if invader and invader.active == True: - if self.invader_move_down == False: + if invader and invader.active: + if not self.invaders_moving_down: invader.move_across(self.invader_direction) else: invader.move_down(self.invader_down_direction) @@ -47,14 +50,14 @@ def update_current_invader_index(self): self.update_movement_flags() def update_movement_flags(self): - if self.invader_move_down == True: - self.invader_move_down = False + if self.invaders_moving_down == True: + self.invaders_moving_down = False # check if any of the invaders have reached screen edge if self.has_reached_horizontal_limits(): # if so switch the direction and set the move_down flag self.invader_direction = self.invader_direction * -1 - self.invader_move_down = True + self.invaders_moving_down = True if self.has_reached_vertical_limit(): self.invaders_landed = True diff --git a/classes/invader/Invader_factory.py b/classes/invader/Invader_factory.py index 1bc29c2..46c7483 100644 --- a/classes/invader/Invader_factory.py +++ b/classes/invader/Invader_factory.py @@ -1,21 +1,38 @@ +import os import pygame -from classes.invader.Invader import Invader # , ModifiedInvader +from classes.invader.Invader import Invader +from classes.config.Invader_config import InvaderConfig class InvaderFactory: - def __init__(self, config): - self.config = config - invader_frames = config.get("invaders")["images"] + def __init__(self): + config = InvaderConfig() - # Load images and update invader_frames dictionary - for invader_size, frames in invader_frames.items(): - loaded_frames = [] - for frame in frames: - image = pygame.image.load(self.config.get_file_path(frame)) - loaded_frames.append(image) - invader_frames[invader_size] = loaded_frames + self.invader_frame_paths = config.get("images") + + self.points_array = config.get("points") + self.spawn_rows = config.get("spawn_rows") + + self.invader_cols = config.get("cols") + + # number of invaders to draw vertically + self.invader_rows = config.get("rows") + + # starting x position of invaders + self.x_position_start = config.get("x_position_start") + + # horizontal offset between each invader + self.x_repeat_offset = config.get("x_repeat_offset") + + # vertical offset between each invader + self.y_repeat_offset = config.get("y_repeat_offset") + + # store a reference to the function in the config + self.get_file_path = config.get_file_path - # invaders build from bottom up + invader_frames = self.load_invader_images() + + # invaders build/drawn upwards on screen self.invader_build_array = [ invader_frames["small"], invader_frames["mid"], @@ -24,46 +41,46 @@ def __init__(self, config): invader_frames["large"], ] - def create_invader_swarm(self): - spawn_rows_pointer = 0 - spawn_rows = self.config.get("invaders")["spawn_rows"] - - # number of invaders to draw horizontally - invader_cols = self.config.get("invaders")["cols"] + def load_invader_images(self): + invader_frames = self.invader_frame_paths + invader_images = {} - # number of invaders to draw vertically - invader_rows = self.config.get("invaders")["rows"] + for invader_size, frames in invader_frames.items(): + loaded_frames = [] + for frame in frames: + _image_path = self.get_file_path(frame) + try: + image = pygame.image.load(_image_path).convert_alpha() + loaded_frames.append(image) + except pygame.error as e: + print(f"Error loading image: {_image_path}") - # starting x position of invaders - x_position_start = self.config.get("invaders")["x_position_start"] + invader_images[invader_size] = loaded_frames - # horizontal offset between each invader - x_repeat_offset = self.config.get("invaders")["x_repeat_offset"] + return invader_images - # vertical offset between each invader - y_repeat_offset = self.config.get("invaders")["y_repeat_offset"] + def create_invader_swarm(self): + spawn_rows_pointer = 0 # starting y position of invaders (change with each wave cleared) - y_position_start = spawn_rows[spawn_rows_pointer] + y_position_start = self.spawn_rows[spawn_rows_pointer] index = 0 - for y_position in range(invader_rows): - for x_position in range(invader_cols): + for y_position in range(self.invader_rows): + for x_position in range(self.invader_cols): yield self.create_invader( - x_position_start + (x_position * x_repeat_offset), - y_position_start - (y_position * y_repeat_offset), + self.x_position_start + (x_position * self.x_repeat_offset), + y_position_start - (y_position * self.y_repeat_offset), True, x_position, 4 - y_position, index, + self.points_array[4 - y_position], ) index += 1 - def create_invader(self, x, y, active, column, row, index): - points_array = self.config.get("invaders")["points"] - points_for_row = points_array[row] - # points = self.config.get("invaders")[row] + def create_invader(self, x, y, active, column, row, index, points): invader_sprite = Invader( x, y, @@ -72,6 +89,6 @@ def create_invader(self, x, y, active, column, row, index): row, self.invader_build_array[row], index, - points_for_row, + points, ) return invader_sprite diff --git a/classes/mothership/Mothership.py b/classes/mothership/Mothership.py index 7fefea7..0bd4aa6 100644 --- a/classes/mothership/Mothership.py +++ b/classes/mothership/Mothership.py @@ -31,7 +31,10 @@ def update(self, shot_counter, dt): return self def update_move(self, dt): - self.rect.x += self.direction * dt + # self.rect.x += self.direction * dt + # print(self.direction * 1 * dt) + # self.rect.x += self.direction * 1 * dt + self.rect.x += self.direction * 1 if self.has_reached_screen_edge(): self.kill() diff --git a/classes/shield/Shield_factory.py b/classes/shield/Shield_factory.py index ee1ac3f..960db2b 100644 --- a/classes/shield/Shield_factory.py +++ b/classes/shield/Shield_factory.py @@ -1,12 +1,16 @@ import pygame from classes.shield.Shield import Shield +from classes.config.Shield_config import ShieldConfig class ShieldFactory: - def __init__(self, config): - image = config.get("shields")["image"] - self.shield_image = pygame.image.load(config.get_file_path(image)) - self.shield_positions = config.get("shields")["positions"] + def __init__(self): + config = ShieldConfig() + image = config.get("image") + image_path = config.get_file_path(image) + + self.shield_image = pygame.image.load(config.get_file_path(image_path)) + self.shield_positions = config.get("positions") def create_shields(self): sprite_group = pygame.sprite.Group() diff --git a/game.py b/game.py index de32ce0..75970c6 100644 --- a/game.py +++ b/game.py @@ -2,21 +2,17 @@ from pygame.locals import * from classes.Game_controller import GameController -from classes.Config import Config -config = Config() pygame.init() -last_time = time.time() -game_controller = GameController(config) -game_controller.load_controllers(config) +game_controller = GameController() +game_controller.load_controllers() running = True - +max_fps = 60 +clock = pygame.time.Clock() while running: - dt = time.time() - last_time - dt *= 60 - last_time = time.time() + dt = 0 events = [] for event in pygame.event.get(): @@ -24,5 +20,9 @@ running = False else: events.append(event) + game_controller.update(events, dt) + pygame.display.flip() + clock.tick(max_fps) + pygame.quit() diff --git a/lib/Controller.py b/lib/Controller.py index ef15094..b2518b6 100644 --- a/lib/Controller.py +++ b/lib/Controller.py @@ -7,9 +7,8 @@ class Controller: - def __init__(self, config): + def __init__(self): self.event_manager = EventManager.get_instance() - self.config = config # Class-level dictionary to store callbacks callbacks = {} diff --git a/test.py b/test.py deleted file mode 100644 index 12dc04c..0000000 --- a/test.py +++ /dev/null @@ -1,125 +0,0 @@ -class MyController: - callbacks = {} # Class-level dictionary to store callbacks - - def __init__(self): - pass - - @classmethod - def register_callback(cls, key, callback): - cls.callbacks[key] = callback - - @classmethod - def get_callback(cls, key): - return cls.callbacks.get(key) - - -class ShieldController(MyController): - def __init__(self): - super().__init__() - self.register_callbacks() - - def get_shields(self): - return ["Shield1", "Shield2"] - - def get_shield_count(self): - return 2 - - def register_callbacks(self): - self.register_callback("get_shields", self.get_shields) - self.register_callback("get_shield_count", self.get_shield_count) - - self.get_invader_callback = self.get_callback("get_invaders") - self.get_bombs_callback = self.get_callback("get_bombs") - - def activate_callback(self): - print("inside shield controller") - bomb_callback = self.get_callback("get_bombs") - invader_callback = self.get_callback("get_invaders") - - invaders = invader_callback() - bombs = bomb_callback() - print(f"Invaders: {invaders}") - print(f"Bombs: {bombs}") - print("-----------") - - -class BombController(MyController): - def __init__(self): - super().__init__() - self.register_callbacks() - - def get_bombs(self): - return ["Bomb1", "Bomb2"] - - def get_bomb_count(self): - return 2 - - def register_callbacks(self): - # Register callbacks specific to BombController - self.register_callback("get_bombs", self.get_bombs) - self.register_callback("get_bomb_count", self.get_bomb_count) - - self.get_invaders_callback = self.get_callback("get_invaders") - self.get_shields_callback = self.get_callback("get_shields") - - def activate_callback(self): - print("inside bomb controller") - invader_callback = self.get_callback("get_invaders") - invaders = invader_callback() - print(f"Invaders: {invaders}") - - shield_callback = self.get_callback("get_shields") - shields = shield_callback() - print(f"Shields: {shields}") - print("-------------") - - -class InvaderController(MyController): - def __init__(self): - super().__init__() - self.register_callbacks() - - def register_callbacks(self): - # Register callbacks specific to BombController - self.register_callback("get_invaders", self.get_invaders) - self.register_callback("get_invader_count", self.get_invader_count) - self.get_bombs_callback = self.get_callback("get_bombs") - - def get_invaders(self): - return ["Invader1", "Invader2", "Invader3"] - - def get_invader_count(self): - return 3 - - def activate_callback(self): - print("inside invader controller") - bomb_callback = self.get_callback("get_bombs") - bombs = bomb_callback() - print(f"Bombs: {bombs}") - print("-----------") - - -# Create an instance of BombController -invader_controller = InvaderController() -bomb_controller = BombController() -shield_controller = ShieldController() - -bomb_controller.activate_callback() -invader_controller.activate_callback() -shield_controller.activate_callback() - -# Retrieve and use the "get_invaders" callback -# get_invaders_callback = invader_controller.get_callback("get_invaders") -# get_bombs_callback = bomb_controller.get_callback("get_bombs") - -# if get_invaders_callback: -# invaders = get_invaders_callback() -# print(f"Invaders: {invaders}") -# else: -# print("Callback not found") - -# if get_bombs_callback: -# bombs = get_bombs_callback() -# print(f"Bombs: {bombs}") -# else: -# print("Callback not found") From 793f3fa91702823503b52ab07a14e48119c419a1 Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Tue, 12 Sep 2023 21:29:56 +0100 Subject: [PATCH 3/6] YABU --- classes/config/Invader_config.py | 2 +- classes/config/UI_config.py | 9 ++++ classes/controllers/Bomb_controller.py | 33 +-------------- classes/controllers/Invader_controller.py | 5 +++ classes/invader/Invader.py | 25 ++++++++++++ classes/invader/Invader_container.py | 29 +++++++++++++ game.py | 2 - test.py | 50 +++++++++++++++++++++++ 8 files changed, 120 insertions(+), 35 deletions(-) create mode 100644 test.py diff --git a/classes/config/Invader_config.py b/classes/config/Invader_config.py index 9c96b21..b862c20 100644 --- a/classes/config/Invader_config.py +++ b/classes/config/Invader_config.py @@ -4,7 +4,7 @@ class InvaderConfig(BaseConfig): config_values = { - "spawn_rows": [120, 144, 160, 168, 168, 168, 176, 176, 176], + "spawn_rows": [170, 144, 160, 168, 168, 168, 176, 176, 176], "points": [30, 20, 20, 10, 10], "cols": 11, "rows": 5, diff --git a/classes/config/UI_config.py b/classes/config/UI_config.py index 085b88e..2b29b9f 100644 --- a/classes/config/UI_config.py +++ b/classes/config/UI_config.py @@ -52,4 +52,13 @@ class UIConfig(BaseConfig): "?": [1, 51], "-": [11, 51], }, + "tints": [ + {"position": (0, 0), "size": (224, 32), "color": (255, 255, 255)}, + {"position": (0, 32), "size": (224, 32), "color": (255, 0, 0)}, + {"position": (0, 64), "size": (224, 120), "color": (255, 255, 255)}, + {"position": (0, 184), "size": (224, 56), "color": (0, 255, 0)}, + {"position": (0, 240), "size": (24, 16), "color": (255, 255, 255)}, + {"position": (24, 240), "size": (112, 16), "color": (0, 255, 0)}, + {"position": (136, 240), "size": (88, 16), "color": (255, 255, 255)}, + ], } diff --git a/classes/controllers/Bomb_controller.py b/classes/controllers/Bomb_controller.py index 15bb126..84efc1c 100644 --- a/classes/controllers/Bomb_controller.py +++ b/classes/controllers/Bomb_controller.py @@ -68,7 +68,7 @@ def create_bomb(self): return self.bomb_factory.create_bomb(invader, bomb_type) def find_attacking_invader(self, bomb_type): - invaders_with_clear_path = self.find_invaders_with_clear_path() + invaders_with_clear_path = self.get_callback("get_invaders_with_clear_path")() if len(invaders_with_clear_path) > 0: if bomb_type == "rolling" and self.get_player_callback: player_rect = self.get_player_callback().get_rect() @@ -82,34 +82,3 @@ def find_attacking_invader(self, bomb_type): return invaders_with_clear_path[ random.randint(0, len(invaders_with_clear_path) - 1) ] - - def find_invaders_with_clear_path(self): - invaders_with_clear_path = [] - invader_callback = self.get_callback("get_invaders") - invader_group = invader_callback() - - # find the lowest screen row (initially row 4) of remaining invaders - # invaders on this row number won't need a path check - max_row = max(invader_group, key=lambda invader: invader.row).row - - for invader in invader_group: - clear_path = True - - # if the invader is on the lowest screen row (highest row number) then don't check any further - if invader.row == max_row: - invaders_with_clear_path.append(invader) - continue - - # else begin inner loop: - # check all invaders against the invader in the outer loop - # if there is an invader with the same column (as the outer loop invader) - # but on a lower screen row (higher row number) then it's not a clear path - for _invader in invader_group: - if _invader.column == invader.column and _invader.row > invader.row: - clear_path = False - break - - if clear_path: - invaders_with_clear_path.append(invader) - - return invaders_with_clear_path diff --git a/classes/controllers/Invader_controller.py b/classes/controllers/Invader_controller.py index 1b25871..6b5db2e 100644 --- a/classes/controllers/Invader_controller.py +++ b/classes/controllers/Invader_controller.py @@ -20,6 +20,11 @@ def __init__(self): "get_invaders", lambda: self.invader_container.get_invaders() ) + self.register_callback( + "get_invaders_with_clear_path", + lambda: self.invader_container.get_invaders_with_clear_path(), + ) + self.register_callback( "get_invader_count", lambda: len(self.invader_container.get_invaders()) ) diff --git a/classes/invader/Invader.py b/classes/invader/Invader.py index 66de452..29be518 100644 --- a/classes/invader/Invader.py +++ b/classes/invader/Invader.py @@ -24,6 +24,31 @@ def __init__(self, x, y, active, column, row, image_frames, index, points): self.rect = self.image.get_rect(topleft=(x, y)) def modify_pixel_colors(self): + tints = [ + {"position": (0, 0), "size": (224, 32), "color": (255, 255, 255)}, + {"position": (0, 32), "size": (224, 32), "color": (255, 0, 0)}, + {"position": (0, 64), "size": (224, 120), "color": (255, 255, 255)}, + {"position": (0, 184), "size": (224, 56), "color": (0, 255, 0)}, + {"position": (0, 240), "size": (24, 16), "color": (255, 255, 255)}, + {"position": (24, 240), "size": (112, 16), "color": (0, 255, 0)}, + {"position": (136, 240), "size": (88, 16), "color": (255, 255, 255)}, + ] + + for frame in self.modified_frames: + print(frame.get_width(), frame.get_height()) + for tint in tints: + position = tint["position"] + size = tint["size"] + color = tint["color"] + + for y in range(position[1], position[1] + size[1]): + for x in range(position[0], position[0] + size[0]): + if 0 <= x < frame.get_width() and 0 <= y < frame.get_height(): + pixel_color = frame.get_at((x, y)) + pixel_color.r, pixel_color.g, pixel_color.b = color + frame.set_at((x, y), pixel_color) + + def _modify_pixel_colors(self): green = (0, 255, 0) white = (255, 255, 255) diff --git a/classes/invader/Invader_container.py b/classes/invader/Invader_container.py index d5504d5..e6bacf8 100644 --- a/classes/invader/Invader_container.py +++ b/classes/invader/Invader_container.py @@ -98,6 +98,35 @@ def get_invaders(self): def get_invader_count(self) -> int: return len(self.sprites()) + def get_invaders_with_clear_path(self): + invaders_with_clear_path = [] + invader_group = self.sprites() + # find the lowest screen row (initially row 4) of remaining invaders + # invaders on this row number won't need a path check + max_row = max(invader_group, key=lambda invader: invader.row).row + + for invader in invader_group: + clear_path = True + + # if the invader is on the lowest screen row (highest row number) then don't check any further + if invader.row == max_row: + invaders_with_clear_path.append(invader) + continue + + # else begin inner loop: + # check all invaders against the invader in the outer loop + # if there is an invader with the same column (as the outer loop invader) + # but on a lower screen row (higher row number) then it's not a clear path + for _invader in invader_group: + if _invader.column == invader.column and _invader.row > invader.row: + clear_path = False + break + + if clear_path: + invaders_with_clear_path.append(invader) + + return invaders_with_clear_path + def has_reached_vertical_limit(self) -> bool: for invader in self.sprites(): if invader.rect.y + invader.rect.height >= self.screen_bottom_limit: diff --git a/game.py b/game.py index 75970c6..15621b4 100644 --- a/game.py +++ b/game.py @@ -1,11 +1,9 @@ import pygame, time from pygame.locals import * - from classes.Game_controller import GameController pygame.init() - game_controller = GameController() game_controller.load_controllers() running = True diff --git a/test.py b/test.py new file mode 100644 index 0000000..f761f4b --- /dev/null +++ b/test.py @@ -0,0 +1,50 @@ +import pygame +import sys + +# Initialize Pygame +pygame.init() + +# Define the screen dimensions +screen_width = 224 +screen_height = 256 +screen = pygame.display.set_mode((screen_width, screen_height)) + +# Define the colors based on your provided areas +colors = [ + (255, 255, 255), # White + (255, 0, 0), # Red + (255, 255, 255), # White + (0, 255, 0), # Green + (255, 255, 255), # White + (0, 255, 0), # Green + (255, 255, 255), # White +] + +# Define the positions and sizes of the areas +areas = [ + {"position": (0, 0), "size": (224, 32)}, + {"position": (0, 32), "size": (224, 32)}, + {"position": (0, 64), "size": (224, 120)}, + {"position": (0, 184), "size": (224, 56)}, + {"position": (0, 240), "size": (24, 16)}, + {"position": (24, 240), "size": (112, 16)}, + {"position": (136, 240), "size": (88, 16)}, +] + +# Main loop +running = True +while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + # Fill the screen with the specified colors + for i, area in enumerate(areas): + color = colors[i] + pygame.draw.rect(screen, color, pygame.Rect(area["position"], area["size"])) + + pygame.display.flip() + +# Quit Pygame +pygame.quit() +sys.exit() From 6f6f69989621b65e103ac4d158320953575e5247 Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Sun, 24 Sep 2023 20:39:10 +0100 Subject: [PATCH 4/6] first commit --- classes/Config.py | 92 ------ classes/Debug.py | 26 -- classes/Game_controller.py | 8 + classes/Modified_sprite.py | 30 -- classes/bomb/Bomb_factory.py | 29 -- classes/config/Bomb_config.py | 24 -- classes/config/Invader_config.py | 17 +- classes/config/Mothership_config.py | 5 - classes/config/Shield_config.py | 4 +- classes/config/UI_config.py | 61 +--- .../{bomb => containers}/Bomb_container.py | 2 +- .../Invader_container.py | 1 - classes/controllers/Baseline_controller.py | 4 - classes/controllers/Bomb_controller.py | 84 ++--- classes/controllers/Invader_controller.py | 6 +- classes/controllers/Mothership_controller.py | 8 +- classes/controllers/Player_controller.py | 17 +- .../controllers/Player_missile_controller.py | 17 +- classes/controllers/Shield_controller.py | 14 +- classes/controllers/UI_controller.py | 15 +- classes/factories/Bomb_factory.py | 39 +++ .../{invader => factories}/Invader_factory.py | 58 ++-- .../{shield => factories}/Shield_factory.py | 11 +- classes/invader/Invader.py | 87 ----- classes/{bomb => models}/Bomb.py | 28 +- classes/models/Invader.py | 47 +++ classes/{mothership => models}/Mothership.py | 36 +-- classes/models/Player.py | 18 ++ classes/models/Player_missile.py | 51 +++ classes/{shield => models}/Shield.py | 12 +- classes/player/Player.py | 20 -- classes/player/Player_missile.py | 73 ----- images/font_spritesheet.png | Bin 829 -> 0 bytes images/sprite_sheet.png | Bin 0 -> 2287 bytes lib/Controller.py | 7 +- lib/Game_sprite.py | 54 ++++ lib/Sprite_sheet.py | 138 ++++++++ space_invaders.ttf | Bin 19380 -> 0 bytes sprites/invader/invader-empty-frame.png | Bin 85 -> 0 bytes sprites/invader/invader-explode.png | Bin 121 -> 0 bytes sprites/invader/invader-large-frame1.png | Bin 139 -> 0 bytes sprites/invader/invader-large-frame2.png | Bin 137 -> 0 bytes sprites/invader/invader-mid-frame1.png | Bin 140 -> 0 bytes sprites/invader/invader-mid-frame2.png | Bin 141 -> 0 bytes sprites/invader/invader-small-frame1.png | Bin 138 -> 0 bytes sprites/invader/invader-small-frame2.png | Bin 134 -> 0 bytes sprites/invader_bomb/bomb_exploding.png | Bin 127 -> 0 bytes sprites/invader_bomb/bomb_exploding_base.png | Bin 89 -> 0 bytes sprites/invader_bomb/explode-stem.png | Bin 93 -> 0 bytes sprites/invader_bomb/plunger-frame1.png | Bin 102 -> 0 bytes sprites/invader_bomb/plunger-frame2.png | Bin 105 -> 0 bytes sprites/invader_bomb/plunger-frame3.png | Bin 105 -> 0 bytes sprites/invader_bomb/plunger-frame4.png | Bin 95 -> 0 bytes sprites/invader_bomb/rolling-frame1.png | Bin 99 -> 0 bytes sprites/invader_bomb/rolling-frame2.png | Bin 108 -> 0 bytes sprites/invader_bomb/rolling-frame3.png | Bin 99 -> 0 bytes sprites/invader_bomb/rolling-frame4.png | Bin 101 -> 0 bytes sprites/invader_bomb/squiggly-frame1.png | Bin 100 -> 0 bytes sprites/invader_bomb/squiggly-frame2.png | Bin 94 -> 0 bytes sprites/invader_bomb/squiggly-frame3.png | Bin 100 -> 0 bytes sprites/invader_bomb/squiggly-frame4.png | Bin 94 -> 0 bytes sprites/mothership/mothership-exploding.png | Bin 163 -> 0 bytes sprites/mothership/mothership.png | Bin 163 -> 0 bytes .../player/player-base-explodes-frame1.png | Bin 134 -> 0 bytes .../player/player-base-explodes-frame2.png | Bin 139 -> 0 bytes sprites/player/player-base.png | Bin 123 -> 0 bytes sprites/player/player-shield.png | Bin 157 -> 0 bytes sprites/player/player-shot-double-height.png | Bin 90 -> 0 bytes sprites/player/player-shot-explodes.png | Bin 116 -> 0 bytes sprites/player/player-shot.png | Bin 90 -> 0 bytes test.py | 296 +++++++++++++++--- 71 files changed, 766 insertions(+), 673 deletions(-) delete mode 100644 classes/Debug.py delete mode 100644 classes/Modified_sprite.py delete mode 100644 classes/bomb/Bomb_factory.py rename classes/{bomb => containers}/Bomb_container.py (93%) rename classes/{invader => containers}/Invader_container.py (98%) create mode 100644 classes/factories/Bomb_factory.py rename classes/{invader => factories}/Invader_factory.py (63%) rename classes/{shield => factories}/Shield_factory.py (56%) delete mode 100644 classes/invader/Invader.py rename classes/{bomb => models}/Bomb.py (63%) create mode 100644 classes/models/Invader.py rename classes/{mothership => models}/Mothership.py (53%) create mode 100644 classes/models/Player.py create mode 100644 classes/models/Player_missile.py rename classes/{shield => models}/Shield.py (94%) delete mode 100644 classes/player/Player.py delete mode 100644 classes/player/Player_missile.py delete mode 100644 images/font_spritesheet.png create mode 100644 images/sprite_sheet.png create mode 100644 lib/Game_sprite.py create mode 100644 lib/Sprite_sheet.py delete mode 100644 space_invaders.ttf delete mode 100644 sprites/invader/invader-empty-frame.png delete mode 100644 sprites/invader/invader-explode.png delete mode 100644 sprites/invader/invader-large-frame1.png delete mode 100644 sprites/invader/invader-large-frame2.png delete mode 100644 sprites/invader/invader-mid-frame1.png delete mode 100644 sprites/invader/invader-mid-frame2.png delete mode 100644 sprites/invader/invader-small-frame1.png delete mode 100644 sprites/invader/invader-small-frame2.png delete mode 100644 sprites/invader_bomb/bomb_exploding.png delete mode 100644 sprites/invader_bomb/bomb_exploding_base.png delete mode 100644 sprites/invader_bomb/explode-stem.png delete mode 100644 sprites/invader_bomb/plunger-frame1.png delete mode 100644 sprites/invader_bomb/plunger-frame2.png delete mode 100644 sprites/invader_bomb/plunger-frame3.png delete mode 100644 sprites/invader_bomb/plunger-frame4.png delete mode 100644 sprites/invader_bomb/rolling-frame1.png delete mode 100644 sprites/invader_bomb/rolling-frame2.png delete mode 100644 sprites/invader_bomb/rolling-frame3.png delete mode 100644 sprites/invader_bomb/rolling-frame4.png delete mode 100644 sprites/invader_bomb/squiggly-frame1.png delete mode 100644 sprites/invader_bomb/squiggly-frame2.png delete mode 100644 sprites/invader_bomb/squiggly-frame3.png delete mode 100644 sprites/invader_bomb/squiggly-frame4.png delete mode 100644 sprites/mothership/mothership-exploding.png delete mode 100644 sprites/mothership/mothership.png delete mode 100644 sprites/player/player-base-explodes-frame1.png delete mode 100644 sprites/player/player-base-explodes-frame2.png delete mode 100644 sprites/player/player-base.png delete mode 100644 sprites/player/player-shield.png delete mode 100644 sprites/player/player-shot-double-height.png delete mode 100644 sprites/player/player-shot-explodes.png delete mode 100644 sprites/player/player-shot.png diff --git a/classes/Config.py b/classes/Config.py index 42e5258..066190e 100644 --- a/classes/Config.py +++ b/classes/Config.py @@ -1,13 +1,10 @@ import os -base_directory = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) - class Config: config_values = { "original_screen_size": (224, 256), "larger_screen_size": (224 * 4, 256 * 4), - "bg_image_path": os.path.join("images", "invaders_moon_bg.png"), "max_fps": 60, "top_left": (0, 0), "ui": { @@ -20,7 +17,6 @@ class Config: }, "shields": { "positions": [(29, 191), (75, 191), (127, 191), (173, 191)], - "image": os.path.join("sprites", "player", "player-shield.png"), }, "invaders": { "spawn_rows": [120, 144, 160, 168, 168, 168, 176, 176, 176], @@ -35,53 +31,12 @@ class Config: "screen_right_limit": 200, "horizontal_move": 2, "vertical_move": 8, - "images": { - "small": [ - os.path.join("sprites", "invader", "invader-small-frame1.png"), - os.path.join("sprites", "invader", "invader-small-frame2.png"), - ], - "mid": [ - os.path.join("sprites", "invader", "invader-mid-frame1.png"), - os.path.join("sprites", "invader", "invader-mid-frame2.png"), - ], - "large": [ - os.path.join("sprites", "invader", "invader-large-frame1.png"), - os.path.join("sprites", "invader", "invader-large-frame2.png"), - ], - }, }, "bombs": { - "exploding_image": os.path.join( - "sprites", "invader_bomb", "bomb_exploding.png" - ), - "images": { - "plunger": [ - os.path.join("sprites", "invader_bomb", "plunger-frame1.png"), - os.path.join("sprites", "invader_bomb", "plunger-frame2.png"), - os.path.join("sprites", "invader_bomb", "plunger-frame3.png"), - os.path.join("sprites", "invader_bomb", "plunger-frame4.png"), - ], - "squiggly": [ - os.path.join("sprites", "invader_bomb", "squiggly-frame1.png"), - os.path.join("sprites", "invader_bomb", "squiggly-frame2.png"), - os.path.join("sprites", "invader_bomb", "squiggly-frame3.png"), - os.path.join("sprites", "invader_bomb", "squiggly-frame4.png"), - ], - "rolling": [ - os.path.join("sprites", "invader_bomb", "rolling-frame1.png"), - os.path.join("sprites", "invader_bomb", "rolling-frame2.png"), - os.path.join("sprites", "invader_bomb", "rolling-frame3.png"), - os.path.join("sprites", "invader_bomb", "rolling-frame4.png"), - ], - }, "max_bombs": 1, "grace_period": 60, }, "mothership": { - "image_frame": os.path.join("sprites", "mothership", "mothership.png"), - "explode_frame": os.path.join( - "sprites", "mothership", "mothership-exploding.png" - ), # "cycles_with_explosion_frame": 60, # "cycles_with_bonus_text": 60, "cycles_until_spawn": 200, @@ -107,49 +62,6 @@ class Config: 300, ], }, - "font_spritesheet_offsets": { - "A": [1, 1], - "B": [11, 1], - "C": [21, 1], - "D": [31, 1], - "E": [41, 1], - "F": [51, 1], - "G": [61, 1], - "H": [71, 1], - "I": [1, 11], - "J": [11, 11], - "K": [21, 11], - "L": [31, 11], - "M": [41, 11], - "N": [51, 11], - "O": [61, 11], - "P": [71, 11], - "Q": [1, 21], - "R": [11, 21], - "S": [21, 21], - "T": [31, 21], - "U": [41, 21], - "W": [51, 21], - "X": [61, 21], - "Y": [1, 31], - "Z": [11, 31], - "0": [21, 31], - "1": [31, 31], - "2": [41, 31], - "3": [51, 31], - "4": [61, 31], - "5": [71, 31], - "6": [1, 41], - "7": [11, 41], - "8": [21, 41], - "9": [31, 41], - "<": [41, 41], - ">": [51, 41], - "=": [61, 41], - "*": [71, 41], - "?": [1, 51], - "-": [11, 51], - }, "audio": { "mothership": "sounds/mothership.wav", "mothership_bonus": "sounds/mothership_bonus.wav", @@ -159,7 +71,3 @@ class Config: @staticmethod def get(key): return Config.config_values.get(key) - - @staticmethod - def get_file_path(key): - return os.path.join(base_directory, key) diff --git a/classes/Debug.py b/classes/Debug.py deleted file mode 100644 index 0355b8b..0000000 --- a/classes/Debug.py +++ /dev/null @@ -1,26 +0,0 @@ -import pygame, os - -font = pygame.font.Font( - os.path.join(os.path.dirname(__file__), "space_invaders.ttf"), 8 -) - - -class Debug: - def __init__(self): - self.debug_requests = [] - - def clear_requests(self): - self.debug_requests = [] - - def add_request(self, x, y, text): - self.debug_requests.append((x, y, text)) - - def get_requests(self): - return self.debug_requests - - def render_requests(self, surface): - for request in self.debug_requests: - x, y, text = request - debug_surf = font.render(text, False, "White") - debug_rect = debug_surf.get_rect(topleft=(x, y)) - surface.blit(debug_surf, debug_rect) diff --git a/classes/Game_controller.py b/classes/Game_controller.py index a23df09..68c8db2 100644 --- a/classes/Game_controller.py +++ b/classes/Game_controller.py @@ -68,6 +68,14 @@ def load_controllers(self): self.controllers.sort(key=lambda controller: controller.rendering_order) + # when all controllers have loaded, call the game_ready function + # which is when it is safe to refer between all controllers + for controller_instance in self.controllers: + if hasattr(controller_instance, "game_ready") and callable( + controller_instance.game_ready + ): + controller_instance.game_ready() + def on_escape_button_pressed(self, data): pygame.quit() diff --git a/classes/Modified_sprite.py b/classes/Modified_sprite.py deleted file mode 100644 index 73dfc77..0000000 --- a/classes/Modified_sprite.py +++ /dev/null @@ -1,30 +0,0 @@ -import pygame.sprite - -# global_position_threshold = 200 - - -class ModifiedSprite(pygame.sprite.Sprite): - def __init__(self, image, x, y): - super().__init__() - self.original_image = image - self.image = self.original_image.copy() # Create a copy to modify - self.rect = self.image.get_rect() - self.rect.topleft = (x, y) - - self.modify_pixel_colors() # Modify pixel colors initially - - def modify_pixel_colors(self): - pixel_array = pygame.PixelArray(self.image) - for y in range(self.rect.height): - for x in range(self.rect.width): - global_x = self.rect.x + x - global_y = self.rect.y + y - - if global_y > 150: - pixel_array[x, y] = (0, 255, 0, pixel_array[x, y][3]) - del pixel_array - - def update(self): - # Handle updates here (e.g., changing position) - # Modify pixel colors after each update - self.modify_pixel_colors() diff --git a/classes/bomb/Bomb_factory.py b/classes/bomb/Bomb_factory.py deleted file mode 100644 index aa3082f..0000000 --- a/classes/bomb/Bomb_factory.py +++ /dev/null @@ -1,29 +0,0 @@ -import pygame -from classes.config.Bomb_config import BombConfig -from classes.bomb.Bomb import Bomb - - -class BombFactory: - def __init__(self): - config = BombConfig() - self.bomb_frames = config.get("images") - - # Load bomb images using config into array - for bomb_type, frames in self.bomb_frames.items(): - loaded_frames = [] - for frame in frames: - image = pygame.image.load(config.get_file_path(frame)) - loaded_frames.append(image) - self.bomb_frames[bomb_type] = loaded_frames - - self.exploding_frame = pygame.image.load( - config.get("exploding_image") - ).convert_alpha() - - def create_bomb(self, invader, bomb_type): - x, y = invader.bomb_launch_position() - - bomb_sprite = Bomb( - x, y, self.bomb_frames[bomb_type], self.exploding_frame, bomb_type - ) - return bomb_sprite diff --git a/classes/config/Bomb_config.py b/classes/config/Bomb_config.py index dc5329b..7d3e3fd 100644 --- a/classes/config/Bomb_config.py +++ b/classes/config/Bomb_config.py @@ -1,32 +1,8 @@ -import os from classes.config.Base_config import BaseConfig class BombConfig(BaseConfig): config_values = { - "exploding_image": os.path.join( - "sprites", "invader_bomb", "bomb_exploding.png" - ), - "images": { - "plunger": [ - os.path.join("sprites", "invader_bomb", "plunger-frame1.png"), - os.path.join("sprites", "invader_bomb", "plunger-frame2.png"), - os.path.join("sprites", "invader_bomb", "plunger-frame3.png"), - os.path.join("sprites", "invader_bomb", "plunger-frame4.png"), - ], - "squiggly": [ - os.path.join("sprites", "invader_bomb", "squiggly-frame1.png"), - os.path.join("sprites", "invader_bomb", "squiggly-frame2.png"), - os.path.join("sprites", "invader_bomb", "squiggly-frame3.png"), - os.path.join("sprites", "invader_bomb", "squiggly-frame4.png"), - ], - "rolling": [ - os.path.join("sprites", "invader_bomb", "rolling-frame1.png"), - os.path.join("sprites", "invader_bomb", "rolling-frame2.png"), - os.path.join("sprites", "invader_bomb", "rolling-frame3.png"), - os.path.join("sprites", "invader_bomb", "rolling-frame4.png"), - ], - }, "max_bombs": 1, "grace_period": 60, } diff --git a/classes/config/Invader_config.py b/classes/config/Invader_config.py index b862c20..198c7f0 100644 --- a/classes/config/Invader_config.py +++ b/classes/config/Invader_config.py @@ -1,10 +1,9 @@ -import os from classes.config.Base_config import BaseConfig class InvaderConfig(BaseConfig): config_values = { - "spawn_rows": [170, 144, 160, 168, 168, 168, 176, 176, 176], + "spawn_rows": [128, 144, 160, 168, 168, 168, 176, 176, 176], "points": [30, 20, 20, 10, 10], "cols": 11, "rows": 5, @@ -16,18 +15,4 @@ class InvaderConfig(BaseConfig): "screen_right_limit": 200, "horizontal_move": 2, "vertical_move": 8, - "images": { - "small": [ - os.path.join("sprites", "invader", "invader-small-frame1.png"), - os.path.join("sprites", "invader", "invader-small-frame2.png"), - ], - "mid": [ - os.path.join("sprites", "invader", "invader-mid-frame1.png"), - os.path.join("sprites", "invader", "invader-mid-frame2.png"), - ], - "large": [ - os.path.join("sprites", "invader", "invader-large-frame1.png"), - os.path.join("sprites", "invader", "invader-large-frame2.png"), - ], - }, } diff --git a/classes/config/Mothership_config.py b/classes/config/Mothership_config.py index fc2b4d3..7fa2e3e 100644 --- a/classes/config/Mothership_config.py +++ b/classes/config/Mothership_config.py @@ -1,13 +1,8 @@ -import os from classes.config.Base_config import BaseConfig class MothershipConfig(BaseConfig): config_values = { - "image_frame": os.path.join("sprites", "mothership", "mothership.png"), - "explode_frame": os.path.join( - "sprites", "mothership", "mothership-exploding.png" - ), # "cycles_with_explosion_frame": 60, # "cycles_with_bonus_text": 60, "cycles_until_spawn": 200, diff --git a/classes/config/Shield_config.py b/classes/config/Shield_config.py index 1e4e383..8aacbe7 100644 --- a/classes/config/Shield_config.py +++ b/classes/config/Shield_config.py @@ -1,9 +1,7 @@ -import os from classes.config.Base_config import BaseConfig class ShieldConfig(BaseConfig): config_values = { - "positions": [(29, 191), (75, 191), (127, 191), (173, 191)], - "image": os.path.join("sprites", "player", "player-shield.png"), + "positions": [(32, 192), (77, 192), (122, 192), (167, 192)], } diff --git a/classes/config/UI_config.py b/classes/config/UI_config.py index 2b29b9f..313ed27 100644 --- a/classes/config/UI_config.py +++ b/classes/config/UI_config.py @@ -9,56 +9,13 @@ class UIConfig(BaseConfig): "hiscore_label_text": "HI-SCORE", "hiscore_label_position": (118, 16), "hiscore_value_position": (118, 32), - "font_spritesheet_offsets": { - "A": [1, 1], - "B": [11, 1], - "C": [21, 1], - "D": [31, 1], - "E": [41, 1], - "F": [51, 1], - "G": [61, 1], - "H": [71, 1], - "I": [1, 11], - "J": [11, 11], - "K": [21, 11], - "L": [31, 11], - "M": [41, 11], - "N": [51, 11], - "O": [61, 11], - "P": [71, 11], - "Q": [1, 21], - "R": [11, 21], - "S": [21, 21], - "T": [31, 21], - "U": [41, 21], - "W": [51, 21], - "X": [61, 21], - "Y": [1, 31], - "Z": [11, 31], - "0": [21, 31], - "1": [31, 31], - "2": [41, 31], - "3": [51, 31], - "4": [61, 31], - "5": [71, 31], - "6": [1, 41], - "7": [11, 41], - "8": [21, 41], - "9": [31, 41], - "<": [41, 41], - ">": [51, 41], - "=": [61, 41], - "*": [71, 41], - "?": [1, 51], - "-": [11, 51], - }, - "tints": [ - {"position": (0, 0), "size": (224, 32), "color": (255, 255, 255)}, - {"position": (0, 32), "size": (224, 32), "color": (255, 0, 0)}, - {"position": (0, 64), "size": (224, 120), "color": (255, 255, 255)}, - {"position": (0, 184), "size": (224, 56), "color": (0, 255, 0)}, - {"position": (0, 240), "size": (24, 16), "color": (255, 255, 255)}, - {"position": (24, 240), "size": (112, 16), "color": (0, 255, 0)}, - {"position": (136, 240), "size": (88, 16), "color": (255, 255, 255)}, - ], + # "tints": [ + # {"position": (0, 0), "size": (224, 32), "color": (255, 255, 255)}, + # {"position": (0, 32), "size": (224, 32), "color": (255, 0, 0)}, + # {"position": (0, 64), "size": (224, 120), "color": (255, 255, 255)}, + # {"position": (0, 184), "size": (224, 56), "color": (0, 255, 0)}, + # {"position": (0, 240), "size": (24, 16), "color": (255, 255, 255)}, + # {"position": (24, 240), "size": (112, 16), "color": (0, 255, 0)}, + # {"position": (136, 240), "size": (88, 16), "color": (255, 255, 255)}, + # ], } diff --git a/classes/bomb/Bomb_container.py b/classes/containers/Bomb_container.py similarity index 93% rename from classes/bomb/Bomb_container.py rename to classes/containers/Bomb_container.py index bb64260..83bfb04 100644 --- a/classes/bomb/Bomb_container.py +++ b/classes/containers/Bomb_container.py @@ -1,5 +1,5 @@ import pygame -from classes.bomb.Bomb import Bomb +from classes.models.Bomb import Bomb class BombContainer(pygame.sprite.Group): diff --git a/classes/invader/Invader_container.py b/classes/containers/Invader_container.py similarity index 98% rename from classes/invader/Invader_container.py rename to classes/containers/Invader_container.py index e6bacf8..d429482 100644 --- a/classes/invader/Invader_container.py +++ b/classes/containers/Invader_container.py @@ -1,5 +1,4 @@ import pygame -from classes.invader.Invader_factory import InvaderFactory from classes.config.Invader_config import InvaderConfig diff --git a/classes/controllers/Baseline_controller.py b/classes/controllers/Baseline_controller.py index 127b54b..2bd3584 100644 --- a/classes/controllers/Baseline_controller.py +++ b/classes/controllers/Baseline_controller.py @@ -6,10 +6,6 @@ class BaselineController(Controller): def __init__(self): self.baselineSprite = pygame.sprite.Sprite() - self.baselineImage = pygame.image.load( - os.path.join("sprites", "invader_bomb", "bomb_exploding_base.png") - ) - # create the surface for the sprite self.baselineSprite.image = pygame.Surface((224, 1), pygame.SRCALPHA) # define the green color as (R, G, B) tuple diff --git a/classes/controllers/Bomb_controller.py b/classes/controllers/Bomb_controller.py index 84efc1c..8dcd4d2 100644 --- a/classes/controllers/Bomb_controller.py +++ b/classes/controllers/Bomb_controller.py @@ -1,10 +1,10 @@ from lib.Controller import Controller -from classes.bomb.Bomb_factory import BombFactory -from classes.bomb.Bomb_container import BombContainer -from classes.invader.Invader import Invader -from classes.bomb.Bomb import Bomb -import random, pygame, os +from classes.factories.Bomb_factory import BombFactory +from classes.containers.Bomb_container import BombContainer +from classes.models.Invader import Invader +from classes.models.Bomb import Bomb +import random class BombController(Controller): @@ -12,34 +12,31 @@ def __init__(self): super().__init__() self.counter = 0 self.enabled = False - self.max_bombs = 1 + self.max_bombs = 2 self.grace_period = 60 self.bomb_types = ["plunger", "squiggly", "rolling"] self.bomb_factory = BombFactory() self.bomb_container = BombContainer() - self.explode_bomb_image = pygame.image.load( - os.path.join("sprites", "invader_bomb", "bomb_exploding.png") - ) + self.event_manager.add_listener( "play_delay_complete", self.on_play_delay_complete ) - self.register_callback("get_bombs", self.get_bombs) - - # self.get_invaders_callback = self.get_callback("get_invaders") - # self.get_player_callback = self.get_callback("get_player") + self.register_callback("get_bombs", lambda: self.bomb_container.get_bombs()) - def get_bombs(self): - return self.bomb_container.get_bombs() + def game_ready(self): + self.get_invaders_callback = self.get_callback("get_invaders") + self.get_player_callback = self.get_callback("get_player") + self.get_invaders_clearpath_callback = self.get_callback( + "get_invaders_with_clear_path" + ) def on_play_delay_complete(self, data): - # print("swarm notify in bomb controller") self.enabled = True def update(self, events, dt): - invader_callback = self.get_callback("get_invaders") - invaders = invader_callback() + invaders = self.get_invaders_callback() if len(invaders) > 0 and self.enabled == True: # create a new bomb if len(self.bomb_container.get_bombs()) < self.max_bombs: @@ -56,29 +53,42 @@ def update(self, events, dt): return self.bomb_container def create_bomb(self): - if self.bomb_container.has_rolling_shot(): - bomb_type = random.choice(self.bomb_types[:2]) - else: - bomb_type = random.choice(self.bomb_types) + def get_next_bomb_type(): + if self.bomb_container.has_rolling_shot(): + bomb_type = random.choice(self.bomb_types[:2]) + else: + bomb_type = random.choice(self.bomb_types) - bomb_type = self.bomb_types[1] + bomb_type = self.bomb_types[1] + return bomb_type + bomb_type = get_next_bomb_type() invader = self.find_attacking_invader(bomb_type) if isinstance(invader, Invader): return self.bomb_factory.create_bomb(invader, bomb_type) def find_attacking_invader(self, bomb_type): - invaders_with_clear_path = self.get_callback("get_invaders_with_clear_path")() - if len(invaders_with_clear_path) > 0: - if bomb_type == "rolling" and self.get_player_callback: - player_rect = self.get_player_callback().get_rect() - for invader in invaders_with_clear_path: - x1 = invader.rect.x + 8 - px1 = player_rect.x - px2 = px1 + 16 - if x1 >= px1 and x1 <= px2 and invader.active == True: - return invader - else: - return invaders_with_clear_path[ - random.randint(0, len(invaders_with_clear_path) - 1) - ] + invaders_with_clear_path = self.get_invaders_clearpath_callback() + + def is_valid_target(invader): + return invader.active + + def is_rolling_bomb(): + return bomb_type == "rolling" and self.get_player_callback is not None + + if is_rolling_bomb(): + player_rect = self.get_player_callback().get_rect() + valid_invaders = [ + invader + for invader in invaders_with_clear_path + if player_rect.x <= (invader.rect.x + 8) <= (player_rect.x + 16) + and is_valid_target(invader) + ] + if valid_invaders: + return random.choice(valid_invaders) + else: + if invaders_with_clear_path: + return random.choice(invaders_with_clear_path) + + # Return None if no valid invader is found + return None diff --git a/classes/controllers/Invader_controller.py b/classes/controllers/Invader_controller.py index 6b5db2e..641624a 100644 --- a/classes/controllers/Invader_controller.py +++ b/classes/controllers/Invader_controller.py @@ -1,6 +1,6 @@ from lib.Controller import Controller -from classes.invader.Invader_factory import InvaderFactory -from classes.invader.Invader_container import InvaderContainer +from classes.factories.Invader_factory import InvaderFactory +from classes.containers.Invader_container import InvaderContainer class InvaderController(Controller): @@ -36,7 +36,7 @@ def __init__(self): def on_invader_hit(self, invader): self.is_moving = False - # # pause invaders 1/4 second (60/15) + # pause invaders 1/4 second (60/15) self.countdown = 15 invader.explode() self.event_manager.notify("points_awarded", invader.points) diff --git a/classes/controllers/Mothership_controller.py b/classes/controllers/Mothership_controller.py index 8f8b693..c22cd79 100644 --- a/classes/controllers/Mothership_controller.py +++ b/classes/controllers/Mothership_controller.py @@ -1,13 +1,15 @@ import pygame from lib.Controller import Controller -from classes.mothership.Mothership import Mothership +from classes.models.Mothership import Mothership from classes.config.Mothership_config import MothershipConfig +from lib.Sprite_sheet import MothershipSpriteSheet class MothershipController(Controller): def __init__(self): super().__init__() self.config = MothershipConfig() + self.sprite_sheet = MothershipSpriteSheet() self.cycles_until_spawn = self.config.get("cycles_until_spawn") self.spawn_right_position = self.config.get("spawn_right_position") self.spawn_left_position = self.config.get("spawn_left_position") @@ -15,6 +17,8 @@ def __init__(self): "qualifying_invader_y_position" ) self.points_table = self.config.get("points_table") + self.mothership_image = self.sprite_sheet.get_sprite("mothership_frame") + self.explode_image = self.sprite_sheet.get_sprite("explode_frame") # set-up non-config related variables self.cycles_lapsed = 0 @@ -112,6 +116,8 @@ def spawn(self): self.spawned = True self.mothership_group.add( Mothership( + self.mothership_image, + self.explode_image, self.get_spawn_position(), self.get_spawn_direction(), self.points_table, diff --git a/classes/controllers/Player_controller.py b/classes/controllers/Player_controller.py index 294f988..717a5c4 100644 --- a/classes/controllers/Player_controller.py +++ b/classes/controllers/Player_controller.py @@ -1,5 +1,6 @@ from lib.Controller import Controller -from classes.player.Player import Player +from classes.models.Player import Player +from lib.Sprite_sheet import PlayerSpriteSheet player_speed = 1 @@ -9,7 +10,19 @@ def __init__(self): super().__init__() self.can_launch_missile = True self.enabled = False - self.player = Player() + sprite_sheet = PlayerSpriteSheet() + + params = { + "player_sprite": sprite_sheet.get_sprite("player"), + "player_explode_sprites": [ + sprite_sheet.get_sprite("player_explode1"), + sprite_sheet.get_sprite("player_explode2"), + ], + "player_x_position": 10, + "player_y_position": 218, + } + + self.player = Player(params) self.left_key_pressed = False self.right_key_pressed = False diff --git a/classes/controllers/Player_missile_controller.py b/classes/controllers/Player_missile_controller.py index a65d38e..88285b0 100644 --- a/classes/controllers/Player_missile_controller.py +++ b/classes/controllers/Player_missile_controller.py @@ -1,11 +1,13 @@ from lib.Controller import Controller -from classes.player.Player_missile import PlayerMissile +from classes.models.Player_missile import PlayerMissile +from lib.Sprite_sheet import PlayerSpriteSheet import pygame class PlayerMissileController(Controller): def __init__(self): super().__init__() + self.sprite_sheet = PlayerSpriteSheet() self.ready_flag = False # there will only ever be one sprite in this group self.missile_group = pygame.sprite.Group() @@ -16,9 +18,8 @@ def __init__(self): self.event_manager.add_listener("play_delay_complete", self.on_missile_ready) self.event_manager.add_listener("fire_button_pressed", self.on_fire_pressed) - # callbacks for interacting with other components - self.get_invaders_callback = self.get_callback("get_invaders") - # self.get_shields_callback = self.get_callback("") + def game_ready(self): + # self.get_invaders_callback = self.get_callback("get_invaders") self.get_player_callback = self.get_callback("get_player") self.mothership_is_exploding = self.get_callback("mothership_is_exploding") @@ -32,7 +33,13 @@ def on_fire_pressed(self, data): and self.ready_flag and not self.mothership_is_exploding() ): - self.missile_group.add(PlayerMissile(player.rect)) + params = { + "missile_sprite": self.sprite_sheet.get_sprite("missile"), + "explode_sprite": self.sprite_sheet.get_sprite("missile_explode"), + "player_x_position": player.rect.x, + "player_y_position": player.rect.y, + } + self.missile_group.add(PlayerMissile(params)) def check_collisions(self): if self.missile_group: diff --git a/classes/controllers/Shield_controller.py b/classes/controllers/Shield_controller.py index a72dc4a..b965c0c 100644 --- a/classes/controllers/Shield_controller.py +++ b/classes/controllers/Shield_controller.py @@ -1,5 +1,6 @@ from lib.Controller import Controller -from classes.shield.Shield_factory import ShieldFactory +from classes.factories.Shield_factory import ShieldFactory +from lib.Sprite_sheet import PlayerSpriteSheet import pygame @@ -7,13 +8,14 @@ class ShieldController(Controller): def __init__(self): super().__init__() - shield_factory = ShieldFactory() + # shield_factory = ShieldFactory() self.rendering_order = -1 - self.shield_container = shield_factory.create_shields() + self.shield_container = ShieldFactory().create_shields() - self.missile_image_2x = pygame.image.load( - "sprites/player/player-shot-double-height.png" - ) + # self.missile_image_2x = pygame.image.load( + # "sprites/player/player-shot-double-height.png" + # ) + self.missile_image_2x = PlayerSpriteSheet().get_sprite("missile") def update(self, events, dt): self.check_bomb_collisions() diff --git a/classes/controllers/UI_controller.py b/classes/controllers/UI_controller.py index 8938e23..33ba0e5 100644 --- a/classes/controllers/UI_controller.py +++ b/classes/controllers/UI_controller.py @@ -1,21 +1,22 @@ import pygame from lib.Controller import Controller from classes.config.UI_config import UIConfig +from lib.Sprite_sheet import FontSpriteSheet class UIController(Controller): def __init__(self): super().__init__() self.config = UIConfig() - self.font_config = self.config.get("font_spritesheet_offsets") - self.spritesheet = pygame.image.load("images/font_spritesheet.png") self.canvas_width = 224 self.canvas_height = 256 self.canvas = pygame.Surface( (self.canvas_width, self.canvas_height), pygame.SRCALPHA ) - + self.sprite_sheet = FontSpriteSheet() self.register_callback("get_score_text", self.create_text_surface) + + def game_ready(self): self.get_score_callback = self.get_callback("get_score") def draw(self, surface): @@ -55,12 +56,8 @@ def create_text_surface(self, text): text_surface = pygame.Surface((surface_width, surface_height), pygame.SRCALPHA) text_surface.fill((0, 0, 0, 0)) - # font_spritesheet_offsets = self.font_config.get("font_spritesheet_offsets") for idx, letter in enumerate(text): - if letter in self.font_config: - letter_x, letter_y = self.font_config[letter] - letter_rect = pygame.Rect(letter_x, letter_y, 8, 8) - letter_image = self.spritesheet.subsurface(letter_rect) - text_surface.blit(letter_image, (idx * 8, 0)) + char_image = self.sprite_sheet.get_sprite(letter) + text_surface.blit(char_image, (idx * 8, 0)) return text_surface diff --git a/classes/factories/Bomb_factory.py b/classes/factories/Bomb_factory.py new file mode 100644 index 0000000..bea890d --- /dev/null +++ b/classes/factories/Bomb_factory.py @@ -0,0 +1,39 @@ +from classes.models.Bomb import Bomb +from lib.Sprite_sheet import BombSpriteSheet + + +class BombFactory: + def __init__(self): + sprite_sheet = BombSpriteSheet() + + self.bomb_frames = {} + self.bomb_frames["squiggly"] = [ + sprite_sheet.get_sprite("squiggly_frame1"), + sprite_sheet.get_sprite("squiggly_frame2"), + sprite_sheet.get_sprite("squiggly_frame3"), + sprite_sheet.get_sprite("squiggly_frame4"), + ] + + self.bomb_frames["rolling"] = [ + sprite_sheet.get_sprite("rolling_frame1"), + sprite_sheet.get_sprite("rolling_frame2"), + sprite_sheet.get_sprite("rolling_frame3"), + sprite_sheet.get_sprite("rolling_frame4"), + ] + + self.bomb_frames["plunger"] = [ + sprite_sheet.get_sprite("plunger_frame1"), + sprite_sheet.get_sprite("plunger_frame2"), + sprite_sheet.get_sprite("plunger_frame3"), + sprite_sheet.get_sprite("plunger_frame4"), + ] + + self.exploding_frame = sprite_sheet.get_sprite("explode_frame") + + def create_bomb(self, invader, bomb_type): + x, y = invader.bomb_launch_position() + + bomb_sprite = Bomb( + x, y, self.bomb_frames[bomb_type], self.exploding_frame, bomb_type + ) + return bomb_sprite diff --git a/classes/invader/Invader_factory.py b/classes/factories/Invader_factory.py similarity index 63% rename from classes/invader/Invader_factory.py rename to classes/factories/Invader_factory.py index 46c7483..3f21ab5 100644 --- a/classes/invader/Invader_factory.py +++ b/classes/factories/Invader_factory.py @@ -1,15 +1,12 @@ -import os -import pygame -from classes.invader.Invader import Invader +from classes.models.Invader import Invader from classes.config.Invader_config import InvaderConfig +from lib.Sprite_sheet import InvaderSpriteSheet class InvaderFactory: def __init__(self): config = InvaderConfig() - self.invader_frame_paths = config.get("images") - self.points_array = config.get("points") self.spawn_rows = config.get("spawn_rows") @@ -27,38 +24,34 @@ def __init__(self): # vertical offset between each invader self.y_repeat_offset = config.get("y_repeat_offset") - # store a reference to the function in the config - self.get_file_path = config.get_file_path - - invader_frames = self.load_invader_images() + sprite_sheet = InvaderSpriteSheet() + self.explode_image = sprite_sheet.get_sprite("invader_explode_frame") # invaders build/drawn upwards on screen + self.invader_build_array = [ - invader_frames["small"], - invader_frames["mid"], - invader_frames["mid"], - invader_frames["large"], - invader_frames["large"], + [ + sprite_sheet.get_sprite("invader_small_frame1"), + sprite_sheet.get_sprite("invader_small_frame2"), + ], + [ + sprite_sheet.get_sprite("invader_small_frame1"), + sprite_sheet.get_sprite("invader_small_frame2"), + ], + [ + sprite_sheet.get_sprite("invader_mid_frame1"), + sprite_sheet.get_sprite("invader_mid_frame2"), + ], + [ + sprite_sheet.get_sprite("invader_large_frame1"), + sprite_sheet.get_sprite("invader_large_frame2"), + ], + [ + sprite_sheet.get_sprite("invader_large_frame1"), + sprite_sheet.get_sprite("invader_large_frame2"), + ], ] - def load_invader_images(self): - invader_frames = self.invader_frame_paths - invader_images = {} - - for invader_size, frames in invader_frames.items(): - loaded_frames = [] - for frame in frames: - _image_path = self.get_file_path(frame) - try: - image = pygame.image.load(_image_path).convert_alpha() - loaded_frames.append(image) - except pygame.error as e: - print(f"Error loading image: {_image_path}") - - invader_images[invader_size] = loaded_frames - - return invader_images - def create_invader_swarm(self): spawn_rows_pointer = 0 @@ -88,6 +81,7 @@ def create_invader(self, x, y, active, column, row, index, points): column, row, self.invader_build_array[row], + self.explode_image, index, points, ) diff --git a/classes/shield/Shield_factory.py b/classes/factories/Shield_factory.py similarity index 56% rename from classes/shield/Shield_factory.py rename to classes/factories/Shield_factory.py index 960db2b..2817919 100644 --- a/classes/shield/Shield_factory.py +++ b/classes/factories/Shield_factory.py @@ -1,16 +1,13 @@ import pygame -from classes.shield.Shield import Shield +from classes.models.Shield import Shield from classes.config.Shield_config import ShieldConfig +from lib.Sprite_sheet import ShieldSpriteSheet class ShieldFactory: def __init__(self): - config = ShieldConfig() - image = config.get("image") - image_path = config.get_file_path(image) - - self.shield_image = pygame.image.load(config.get_file_path(image_path)) - self.shield_positions = config.get("positions") + self.shield_image = ShieldSpriteSheet().get_sprite("shield_frame") + self.shield_positions = ShieldConfig().get("positions") def create_shields(self): sprite_group = pygame.sprite.Group() diff --git a/classes/invader/Invader.py b/classes/invader/Invader.py deleted file mode 100644 index 29be518..0000000 --- a/classes/invader/Invader.py +++ /dev/null @@ -1,87 +0,0 @@ -import pygame.sprite - - -class Invader(pygame.sprite.Sprite): - def __init__(self, x, y, active, column, row, image_frames, index, points): - super().__init__() - - self.index = index - self.row = row - self.column = column - self.frame_pointer = 0 - self.image_frames = image_frames - self.active = active - self.points = points - - self.explode_frame = pygame.image.load( - "sprites/invader/invader-explode.png" - ).convert_alpha() - - # Create copies of the image frames to modify without affecting originals - self.modified_frames = [frame.copy() for frame in self.image_frames] - - self.image = self.modified_frames[0] # Start with the modified copy - self.rect = self.image.get_rect(topleft=(x, y)) - - def modify_pixel_colors(self): - tints = [ - {"position": (0, 0), "size": (224, 32), "color": (255, 255, 255)}, - {"position": (0, 32), "size": (224, 32), "color": (255, 0, 0)}, - {"position": (0, 64), "size": (224, 120), "color": (255, 255, 255)}, - {"position": (0, 184), "size": (224, 56), "color": (0, 255, 0)}, - {"position": (0, 240), "size": (24, 16), "color": (255, 255, 255)}, - {"position": (24, 240), "size": (112, 16), "color": (0, 255, 0)}, - {"position": (136, 240), "size": (88, 16), "color": (255, 255, 255)}, - ] - - for frame in self.modified_frames: - print(frame.get_width(), frame.get_height()) - for tint in tints: - position = tint["position"] - size = tint["size"] - color = tint["color"] - - for y in range(position[1], position[1] + size[1]): - for x in range(position[0], position[0] + size[0]): - if 0 <= x < frame.get_width() and 0 <= y < frame.get_height(): - pixel_color = frame.get_at((x, y)) - pixel_color.r, pixel_color.g, pixel_color.b = color - frame.set_at((x, y), pixel_color) - - def _modify_pixel_colors(self): - green = (0, 255, 0) - white = (255, 255, 255) - - for frame in self.modified_frames: - for y in range(frame.get_height()): - for x in range(frame.get_width()): - pixel_color = frame.get_at((x, y)) - if y + self.rect.y >= 191: - pixel_color.r, pixel_color.g, pixel_color.b = green - else: - pixel_color.r, pixel_color.g, pixel_color.b = white - - frame.set_at((x, y), pixel_color) - - def bomb_launch_position(self): - return (self.rect.x + 7, self.rect.y + 8) - - def explode(self): - self.image = self.explode_frame - self.active = False - - def release(self): - self.kill() - - def move_across(self, direction): - self.image = self.get_sprite_image() - self.rect.x += direction - - def move_down(self, direction): - self.image = self.get_sprite_image() - self.rect.y += direction - self.modify_pixel_colors() - - def get_sprite_image(self): - self.frame_pointer = 1 - self.frame_pointer - return self.modified_frames[self.frame_pointer] # Use the modified copy diff --git a/classes/bomb/Bomb.py b/classes/models/Bomb.py similarity index 63% rename from classes/bomb/Bomb.py rename to classes/models/Bomb.py index 3254808..4948844 100644 --- a/classes/bomb/Bomb.py +++ b/classes/models/Bomb.py @@ -1,7 +1,7 @@ -import pygame.sprite +from lib.Game_sprite import GameSprite -class Bomb(pygame.sprite.Sprite): +class Bomb(GameSprite): def __init__(self, x, y, image_frames, explode_frame, bomb_type): super().__init__() self.image_frames = image_frames @@ -20,21 +20,6 @@ def __init__(self, x, y, image_frames, explode_frame, bomb_type): self.rect.x = x self.rect.y = y - def modify_pixel_colors(self): - green = (0, 255, 0) - white = (255, 255, 255) - - for frame in self.modified_frames: - for y in range(frame.get_height()): - for x in range(frame.get_width()): - pixel_color = frame.get_at((x, y)) - if y + self.rect.y >= 191: - pixel_color.r, pixel_color.g, pixel_color.b = green - else: - pixel_color.r, pixel_color.g, pixel_color.b = white - - frame.set_at((x, y), pixel_color) - def explode(self): self.image = self.explode_frame @@ -58,14 +43,13 @@ def update(self): if self.rect.y <= 233: self.rect.y += 2 * 1.4 # 3 * 1.4 - self.modify_pixel_colors() - # if self.rect.y > 232: - # self.rect.y = 232 - # # self.kill() + if self.rect.y > 232: + self.rect.y = 232 + self.kill() else: self.countdown -= 1 if self.countdown == 0: self.kill() def get_sprite_image(self): - return self.modified_frames[self.frame_pointer] # Use the modified copy + return self.modify_pixel_colors(self.modified_frames[self.frame_pointer]) diff --git a/classes/models/Invader.py b/classes/models/Invader.py new file mode 100644 index 0000000..c9ed699 --- /dev/null +++ b/classes/models/Invader.py @@ -0,0 +1,47 @@ +from lib.Game_sprite import GameSprite + + +class Invader(GameSprite): + def __init__( + self, x, y, active, column, row, image_frames, explode_image, index, points + ): + super().__init__() + + self.index = index + self.row = row + self.column = column + self.frame_pointer = 0 + self.image_frames = image_frames + self.explode_frame = explode_image + self.active = active + self.points = points + + # Create copies of the image frames to modify without affecting originals + self.modified_frames = [frame.copy() for frame in self.image_frames] + + self.image = self.modified_frames[0] # Start with the modified copy + self.rect = self.image.get_rect(topleft=(x, y)) + + def bomb_launch_position(self): + return (self.rect.x + 7, self.rect.y + 8) + + def explode(self): + self.image = self.explode_frame + self.active = False + + def release(self): + self.kill() + + def move_across(self, direction): + self.image = self.get_sprite_image() + self.rect.x += direction + + def move_down(self, direction): + self.rect.y += direction + self.image = self.get_sprite_image() + + def get_sprite_image(self): + self.frame_pointer = 1 - self.frame_pointer + return self.modify_pixel_colors( + self.modified_frames[self.frame_pointer] + ) # Use the modified copy diff --git a/classes/mothership/Mothership.py b/classes/models/Mothership.py similarity index 53% rename from classes/mothership/Mothership.py rename to classes/models/Mothership.py index 0bd4aa6..789476b 100644 --- a/classes/mothership/Mothership.py +++ b/classes/models/Mothership.py @@ -1,13 +1,14 @@ -import pygame.sprite +from lib.Game_sprite import GameSprite -class Mothership(pygame.sprite.Sprite): - def __init__(self, spawn_position, direction, points_table): +class Mothership(GameSprite): + def __init__( + self, mothership_image, explode_image, spawn_position, direction, points_table + ): super().__init__() - self.sprite_path = "sprites/mothership/mothership.png" - self.sprite_path_explode = "sprites/mothership/mothership-exploding.png" self.points_table = points_table - self.image = pygame.image.load(self.sprite_path).convert_alpha() + self.image = mothership_image + self.explode_image = explode_image self.rect = self.image.get_rect(topleft=spawn_position) self.direction = direction self.shot_counter = 0 @@ -20,7 +21,7 @@ def explode(self, score_text_surface): self.rect.x = min(self.rect.x, 208) self.active = False self.points_image = score_text_surface - self.image = pygame.image.load(self.sprite_path_explode).convert_alpha() + self.image = self.explode_image def update(self, shot_counter, dt): self.shot_counter = shot_counter @@ -31,9 +32,6 @@ def update(self, shot_counter, dt): return self def update_move(self, dt): - # self.rect.x += self.direction * dt - # print(self.direction * 1 * dt) - # self.rect.x += self.direction * 1 * dt self.rect.x += self.direction * 1 if self.has_reached_screen_edge(): @@ -55,20 +53,4 @@ def has_reached_screen_edge(self): ) def draw(self, surface): - self.modify_pixel_colors() - surface.blit(self.image, self.rect) - - # in the arcade game a red filter was applied - def modify_pixel_colors(self): - red = (255, 0, 0) - white = (255, 255, 255) - for y in range(self.image.get_height()): - for x in range(self.image.get_width()): - pixel_color = self.image.get_at((x, y)) - if ( - pixel_color[0] == 255 - and pixel_color[1] == 255 - and pixel_color[2] == 255 - ): - pixel_color.r, pixel_color.g, pixel_color.b = red - self.image.set_at((x, y), pixel_color) + surface.blit(self.modify_pixel_colors(self.image), self.rect) diff --git a/classes/models/Player.py b/classes/models/Player.py new file mode 100644 index 0000000..0b13c78 --- /dev/null +++ b/classes/models/Player.py @@ -0,0 +1,18 @@ +from lib.Game_sprite import GameSprite + + +class Player(GameSprite): + def __init__(self, params): + super().__init__() + + self.image = params.get("player_sprite") + self.rect = self.image.get_rect( + x=params.get("player_x_position"), y=params.get("player_y_position") + ) + + # used by BombController + def get_rect(self): + return self.rect + + def draw(self, surface): + surface.blit(self.modify_pixel_colors(self.image), self.rect) diff --git a/classes/models/Player_missile.py b/classes/models/Player_missile.py new file mode 100644 index 0000000..138ab91 --- /dev/null +++ b/classes/models/Player_missile.py @@ -0,0 +1,51 @@ +from lib.Game_sprite import GameSprite + + +class PlayerMissile(GameSprite): + def __init__(self, params): + super().__init__() + + self.delay = 1 + self.image = params.get("missile_sprite") + self.explode_frame = params.get("explode_sprite") + self.countdown = 0 + self.active = True + self.rect = self.image.get_rect( + x=params.get("player_x_position") + 8, y=params.get("player_y_position") + ) + + def draw(self, surface): + surface.blit(self.modify_pixel_colors(self.image), self.rect) + + def remove(self): + self.kill() + + def explode(self, position_rect=None): + if self.active: + self.image = self.explode_frame + if position_rect: + print("updated rect") + self.rect.x = position_rect[0] + self.rect.y = position_rect[1] + + # print(self.rect) + # if position_rect + # self.rect.x -= 4 + # self.rect.y -= 3 + self.active = False + self.countdown = 30 + + def update(self): + if self.countdown > 0: + self.countdown -= 1 + if self.countdown <= 0: + self.kill() + else: + # if self.delay <= 0: + self.delay = 1 + self.rect.y -= 4 # Move the missile vertically upwards + if self.rect.y <= 42: + self.explode(()) + # else: + # self.delay -= 1 + return self diff --git a/classes/shield/Shield.py b/classes/models/Shield.py similarity index 94% rename from classes/shield/Shield.py rename to classes/models/Shield.py index 7f4896e..624c66c 100644 --- a/classes/shield/Shield.py +++ b/classes/models/Shield.py @@ -1,7 +1,9 @@ import pygame +from lib.Game_sprite import GameSprite -class Shield(pygame.sprite.Sprite): + +class Shield(GameSprite): def __init__(self, x, y, image): super().__init__() @@ -12,6 +14,7 @@ def __init__(self, x, y, image): self.rect.y = y # create the mask from the shield image # used in collision detection + self.modify_pixel_colors(self.image) self.mask = pygame.mask.from_surface(self.image) def missile_damage(self, missile_sprite): @@ -53,7 +56,6 @@ def bomb_damage(self, bomb_sprite): global_position[0] - shield_rect.x, global_position[1] - shield_rect.y, ) - # print(local_position) bomb_type = bomb_sprite.bomb_type # if bomb_type == "plunger": @@ -65,7 +67,7 @@ def bomb_damage(self, bomb_sprite): modified_shield_surface = self.image.copy() modified_shield_surface.blit( - bomb_sprite.explode_frame, + self.modify_pixel_colors(bomb_sprite.explode_frame), # (local_position[0], 0 - y_adjust), (local_position[0], local_position[1]), special_flags=pygame.BLEND_RGBA_SUB, @@ -73,7 +75,9 @@ def bomb_damage(self, bomb_sprite): self.image = modified_shield_surface # update the sprite mask so future collisions # use the mask rather than a basic rect - self.mask = pygame.mask.from_surface(modified_shield_surface) + self.mask = pygame.mask.from_surface( + self.modify_pixel_colors(modified_shield_surface) + ) def invader_damage(self, invader_sprite): shield_rect = self.rect diff --git a/classes/player/Player.py b/classes/player/Player.py deleted file mode 100644 index 21da71b..0000000 --- a/classes/player/Player.py +++ /dev/null @@ -1,20 +0,0 @@ -import pygame.sprite - - -class Player(pygame.sprite.Sprite): - def __init__(self): - super().__init__() - - sprite_path = "sprites/player/player-base.png" - self.image = pygame.image.load(sprite_path).convert_alpha() - self.rect = self.image.get_rect() - self.rect.x = 10 - self.rect.y = 218 - self.target_pos = self.rect.center - - # used by BombController - def get_rect(self): - return self.rect - - def draw(self, surface): - surface.blit(self.image, self.rect) diff --git a/classes/player/Player_missile.py b/classes/player/Player_missile.py deleted file mode 100644 index 2a54ff5..0000000 --- a/classes/player/Player_missile.py +++ /dev/null @@ -1,73 +0,0 @@ -import os -import pygame.sprite - - -class PlayerMissile(pygame.sprite.Sprite): - def __init__(self, player_rect): - super().__init__() - - self.delay = 1 - sprite_path = "sprites/player/player-shot.png" - self.image = pygame.image.load(sprite_path).convert_alpha() - self.countdown = 0 - self.active = True - self.rect = self.image.get_rect() - self.rect.x = player_rect.x + 8 - self.rect.y = player_rect.y - self.explode_frame = pygame.image.load( - os.path.join("sprites", "player", "player-shot-explodes.png") - ) - - def modify_pixel_colors(self): - green = (0, 255, 0) - white = (255, 255, 255) - red = (255, 0, 0) - - for y in range(self.image.get_height()): - for x in range(self.image.get_width()): - pixel_color = self.image.get_at((x, y)) - if y + self.rect.y >= 191: - pixel_color.r, pixel_color.g, pixel_color.b = white - elif y + self.rect.y <= 35: - pixel_color.r, pixel_color.g, pixel_color.b = red - else: - pixel_color.r, pixel_color.g, pixel_color.b = green - - self.image.set_at((x, y), pixel_color) - - def draw(self, surface): - self.modify_pixel_colors() - surface.blit(self.image, self.rect) - - def remove(self): - self.kill() - - def explode(self, position_rect=None): - if self.active: - self.image = self.explode_frame - if position_rect: - print("updated rect") - self.rect.x = position_rect[0] - self.rect.y = position_rect[1] - - print(self.rect) - # if position_rect - # self.rect.x -= 4 - # self.rect.y -= 3 - self.active = False - self.countdown = 30 - - def update(self): - if self.countdown > 0: - self.countdown -= 1 - if self.countdown <= 0: - self.kill() - else: - # if self.delay <= 0: - self.delay = 1 - self.rect.y -= 4 # Move the missile vertically upwards - if self.rect.y <= 42: - self.explode(()) - # else: - # self.delay -= 1 - return self diff --git a/images/font_spritesheet.png b/images/font_spritesheet.png deleted file mode 100644 index c600e9f3af4cafde7465e6d3642b5544742e6db0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 829 zcmV-D1H$}?P)Px%_en%SRCt{2T+x!kAPl6AAJ$Lz&$x%1jFbklOHzv?`{F5a6C#9ctGDg@p3}lO zkOc1go|m0&_pD#cIp=NLw$__+&Ie-Dwq=L%)pjjg)DHDs=c9i9)Qqhp_6SkOEMIGa zqjuIDQ9Lw4Nurm$L`1MN;n*0!7DfosEf`@yE zm;jg{XM|_Imu=y?jz(-brUS4LRp+dyv;rJJgTY zuZ&sq%@ykL(GNp z{_J+N60tgSFP~RoeA@0=4vgHJXxX8B^4eWJqZc2mf!~;%l#Zvh_90JkLG?}`E5Fx< zomL>y^r9CXP_aHcBzf8#P6uxrG@%CVq{Z zifbtmAC?mFV)jmdc00O})@lWEmBp$@NXf_m(*0@352bspJBm#r3JXT@dt(^j-=3p386FAy+% zVuzNF(O8G>iLjH1KvcO9Yz5@`2%r06Ld!X$92n^p=@)^;x0HwzONn?fd#69Uo%%Zw z`TJ8!gsoXo{T+iAr}{gQ9#OOsVH67!V^k8Y_?lXHg#2o-Efgysty5j&YyQju>GXRx zXgh(bl#vgsmqdoE$8(Z$NWjQsn5|jcv6Db!sK-Fj`cUh&V-7jIdjam4|(6_S|-Z@WUMW=N_OZM!00000NkvXX Hu0mjfkVuS! diff --git a/images/sprite_sheet.png b/images/sprite_sheet.png new file mode 100644 index 0000000000000000000000000000000000000000..8f9341e737fdc2b40c8b1e2f6bf7978a90f4d5df GIT binary patch literal 2287 zcmV00001b5ch_0Itp) z=>Px-sYygZRCt{2o$Z#ZItYg2taD*{<@U~U{_M=A6v9h_nsn!x)itdm`M{8X+HU*4 z=d_rT|58fn^?GfZK+ZX*l+w0ss!TZnBt1FjEFCLtuj!$6DQI=_)JLly)iWwTwVkxH zc*j{#qw`~Fhgo_v!Lv_m2d$4}NTh9}pc+r>EN=m{qi9DXx;*98WcdkH42F)tQ4x(N zxk_}TSbAD@ZGX@{Bgbpy&1krk{>*tAJ#JIe)2gdg2R(r_1M!n-7DI~0qm7Z?Bh=1x zsVr+B?^v{>5!YC@&tkMfbDs?ELhy)_Co4?^qUZQBTJqsKA}oobx77@M!ouzWkNP@!c$kmeHayov0aX|9Sw zHJ;RIEe7N(JC81JLf}aQp6B`Y`8bZZZO3t_a(sK)X7SHtSe}9#hZ;uOhZ-)W8xqsf zu`PN_e5ZXL1>X6R(Q5FKSwl3Q6hwOiYeW{=FO6Dc+h`z0@fa?pirb{d;z6yegrsL! z5UA@d<T+lELO&_VbD6d{k+w$$$Sted8q&PTw~e&m@$i#UpP_mTr)6I}$682rqed(B zpV5H7p+c*n(1uHf>KSF;QDY?vx<~r9-TO+!cs+(f3>fVWxn2#;ZH^FL^m56Z*W(UR zdfjGiB+kMmbKXYa+odv)ZJJE$e2LU;bl&DXNx*94U5(a^+;W1Hlajjf?va=Cy|1(~ zEMYtmq_2BeCT7a?n0BvBi!RB{&f<-}V&wJAS2M0M3)34fqVs1p;MB&8?DQE8IIZ!n z=j5jgnvkC~(GX^yMM6a1?=`=}(%g+*j-G%FR_muElCN)IuauB~{ zqZw}T$%or4SC_Q|kp1O9zxqHa+qJ6$%WyVt#`7$dt&u5R-{Z&gQO6BC+tU4R-mavl zWIsYo-y@wXjWX#{jAZQAOEp^suP(UEoBvWGSJF!*dfrp00l2YiZj*Pix5F*@Z*s@K zSJI^mmb?=!nUw6ux@vCKax%jf*}Ig6PvN1n7o^dfaW66o_| zYw@Bs*K$2lY#A}XX7U%I!}?3CD`bRPqsdb) z)g`Ss5&fgFGAt9bGAt7VWmv2X3P*Ey->&Cw#M>Z{12wsprFw0?L(A&XgXkJD1POJU zR_EQJ$LeHHeP~?TqvtBRj7NiH$)$K!b1i!4%8`cv-ul#Z^|b3sx{O!iD)C{P}@$C`1 ziu*%OC`&g|A4^wHeXbyE!J;)tb3+=^NNnk_(du&rE5kA|E5kA|p&6EcRqiRoE9uti zkbXvGH91tD7Mr!op=I;_@Xnc6@@70fIco={6}eQ)KBI$&eopn$T=SBqKGNGpkF#%x&^{Gs4Yz zR=O5f%B_JKah~Ugae9rX$ro0JB^c>9@X))ly+7Z!GAsdd z6z}ilA(Tqp2`W}`*^3AW6VF{5-Dc&Vo?5QEc;>kF)=7Ip^VF?1dsu0M&elvD%R)!@& zE~Vel>o$&^%z1o{(B=$jZqViy&zy(MQHVZI{gAR-^xcI!&f^ugGA!C>L`RE0w0@ql zM==)Xcg}0cBzWX%`w<;29f*bsh@OJC4p;K=tilzBi#&5nCwg3LPrWvVs`r81W^;P% zDZ`=qiX7^$M@)ZNG~L7E?Z614Ty%ksrt-N*-xG8vrc;s|?#YOF(piz^B1G$pWMx=_ zSog3jT&xUBfZS84Klobvt#O_pqSc-4Jh>f_jD8s7EtTdE0J%rPT3orPYz8(FIMG%e9tQ)2;2})b!B*qD+V@ z)L*9CMBM|@ksf!$LJNq?P)gWOnvDiUS81AjAmMp(S+*Z|GC`$z%O5l z_pp@DJ&I9%-ZBvbMvf4!XOzWCN-M)MH7mn1F_&am7Wbfj>)%MeeIet%0ckM~Z}+g2 z_sei|(ESDBDJ>WK6y3M}EuKSsO6ZA-Ec`V8a^>QAR)%F_{s-I=ZNoo4CGG$K002ov JPDHLkV1hblleYi> literal 0 HcmV?d00001 diff --git a/lib/Controller.py b/lib/Controller.py index b2518b6..0d1cbf7 100644 --- a/lib/Controller.py +++ b/lib/Controller.py @@ -1,10 +1,5 @@ -import pygame from lib.Event_manager import EventManager -import importlib -import os -import inspect - class Controller: def __init__(self): @@ -39,4 +34,4 @@ def debug_callbacks(cls): ) -pygame.quit() +# pygame.quit() diff --git a/lib/Game_sprite.py b/lib/Game_sprite.py new file mode 100644 index 0000000..531fd3e --- /dev/null +++ b/lib/Game_sprite.py @@ -0,0 +1,54 @@ +import pygame + + +class GameSprite(pygame.sprite.Sprite): + def __init__(self): + super().__init__() + + def modify_pixel_colors(self, image): + if isinstance(image, pygame.Surface): + return self.apply_image_tints(image) + else: + print("not an image being converted") + + def apply_image_tints(self, image): + area_colors = [0, 1, 0, 3, 0, 3, 0] # Corresponding colors for each area + areas = [ + {"position": (0, 0), "size": (224, 32)}, + {"position": (0, 32), "size": (224, 32)}, + {"position": (0, 64), "size": (224, 120)}, + {"position": (0, 184), "size": (224, 56)}, + {"position": (0, 240), "size": (24, 16)}, + {"position": (24, 240), "size": (112, 16)}, + {"position": (136, 240), "size": (88, 16)}, + ] + + # banding colours + colors = [ + (255, 255, 255), # White + (255, 0, 0), # Red + (255, 255, 255), # White + (0, 255, 0), # Green + (255, 255, 255), # White + (0, 255, 0), # Green + (255, 255, 255), # White + ] + + for y in range(image.get_height()): + for x in range(image.get_width()): + pixel_color = image.get_at((x, y)) + pixel_x, pixel_y = ( + self.rect.x + x, + self.rect.y + y, + ) # Pixel position in the sprite's coordinate system + + for i, area in enumerate(areas): + area_rect = pygame.Rect(area["position"], area["size"]) + if area_rect.collidepoint(pixel_x, pixel_y): + color_index = area_colors[i] + new_color = colors[color_index] + pixel_color.r, pixel_color.g, pixel_color.b = new_color + + image.set_at((x, y), pixel_color) + + return image diff --git a/lib/Sprite_sheet.py b/lib/Sprite_sheet.py new file mode 100644 index 0000000..208c3d3 --- /dev/null +++ b/lib/Sprite_sheet.py @@ -0,0 +1,138 @@ +import pygame + + +class SpriteSheet: + def __init__(self): + self.sprite_sheet = pygame.image.load("images/sprite_sheet.png").convert_alpha() + self.sprite_lookup = {} + + def add_sprite(self, name, x, y, width, height): + self.sprite_lookup[name] = (x, y, width, height) + + def get_sprite(self, name): + sprite_rect = self.sprite_lookup.get(name) + if sprite_rect: + x, y, width, height = sprite_rect + sprite = pygame.Surface((width, height), pygame.SRCALPHA) + sprite.blit(self.sprite_sheet, (0, 0), (x, y, width, height)) + return sprite + else: + raise ValueError(f"Sprite with name '{name}' not found in the spritesheet.") + + +class InvaderSpriteSheet(SpriteSheet): + def __init__(self): + super().__init__() + + self.add_sprite("invader_small_frame1", 1, 1, 16, 8) + self.add_sprite("invader_small_frame2", 1, 11, 16, 8) + + self.add_sprite("invader_mid_frame1", 19, 1, 16, 8) + self.add_sprite("invader_mid_frame2", 19, 11, 16, 8) + + self.add_sprite("invader_large_frame1", 37, 1, 16, 8) + self.add_sprite("invader_large_frame2", 37, 11, 16, 8) + + self.add_sprite("invader_explode_frame", 55, 1, 16, 8) + + +class BombSpriteSheet(SpriteSheet): + def __init__(self): + super().__init__() + + self.add_sprite("plunger_frame1", 21, 21, 3, 8) + self.add_sprite("plunger_frame2", 26, 21, 3, 8) + self.add_sprite("plunger_frame3", 31, 21, 3, 8) + self.add_sprite("plunger_frame4", 36, 21, 3, 8) + + self.add_sprite("squiggly_frame1", 1, 21, 3, 8) + self.add_sprite("squiggly_frame2", 6, 21, 3, 8) + self.add_sprite("squiggly_frame3", 11, 21, 3, 8) + self.add_sprite("squiggly_frame4", 16, 21, 3, 8) + + self.add_sprite("rolling_frame1", 41, 21, 3, 8) + self.add_sprite("rolling_frame2", 46, 21, 3, 8) + self.add_sprite("rolling_frame3", 51, 21, 3, 8) + self.add_sprite("rolling_frame4", 56, 21, 3, 8) + + self.add_sprite("explode_frame", 61, 21, 6, 8) + + +class ShieldSpriteSheet(SpriteSheet): + def __init__(self): + super().__init__() + + self.add_sprite("shield_frame", 45, 31, 24, 16) + + +class MothershipSpriteSheet(SpriteSheet): + def __init__(self): + super().__init__() + + self.add_sprite("mothership_frame", 1, 39, 16, 8) + self.add_sprite("explode_frame", 19, 39, 24, 8) + + +class PlayerSpriteSheet(SpriteSheet): + def __init__(self): + super().__init__() + self.add_sprite("player", 1, 49, 16, 8) + self.add_sprite("player_explode1", 19, 49, 16, 8) + self.add_sprite("player_explode2", 37, 49, 16, 8) + self.add_sprite("missile", 55, 49, 1, 8) + self.add_sprite("missile_explode", 58, 49, 8, 8) + + +class FontSpriteSheet(SpriteSheet): + def __init__(self): + super().__init__() + + self.add_sprite("A", 1, 69, 8, 8) + self.add_sprite("B", 11, 69, 8, 8) + self.add_sprite("C", 21, 69, 8, 8) + self.add_sprite("D", 31, 69, 8, 8) + self.add_sprite("E", 41, 69, 8, 8) + self.add_sprite("F", 51, 69, 8, 8) + self.add_sprite("G", 61, 69, 8, 8) + self.add_sprite("H", 71, 69, 8, 8) + + self.add_sprite("I", 1, 79, 8, 8) + self.add_sprite("J", 11, 79, 8, 8) + self.add_sprite("K", 21, 79, 8, 8) + self.add_sprite("L", 31, 79, 8, 8) + self.add_sprite("M", 41, 79, 8, 8) + self.add_sprite("N", 51, 79, 8, 8) + self.add_sprite("O", 61, 79, 8, 8) + self.add_sprite("P", 71, 79, 8, 8) + + self.add_sprite("Q", 1, 89, 8, 8) + self.add_sprite("R", 11, 89, 8, 8) + self.add_sprite("S", 21, 89, 8, 8) + self.add_sprite("T", 31, 89, 8, 8) + self.add_sprite("U", 41, 89, 8, 8) + self.add_sprite("V", 51, 89, 8, 8) + self.add_sprite("W", 61, 89, 8, 8) + self.add_sprite("X", 71, 89, 8, 8) + + self.add_sprite("Y", 1, 99, 8, 8) + self.add_sprite("Z", 11, 99, 8, 8) + self.add_sprite("0", 21, 99, 8, 8) + self.add_sprite("1", 31, 99, 8, 8) + self.add_sprite("2", 41, 99, 8, 8) + self.add_sprite("3", 51, 99, 8, 8) + self.add_sprite("4", 61, 99, 8, 8) + self.add_sprite("5", 71, 99, 8, 8) + + self.add_sprite("6", 1, 109, 8, 8) + self.add_sprite("7", 11, 109, 8, 8) + self.add_sprite("8", 21, 109, 8, 8) + self.add_sprite("9", 31, 109, 8, 8) + + self.add_sprite("<", 41, 109, 8, 8) + self.add_sprite(">", 51, 109, 8, 8) + self.add_sprite("=", 61, 109, 8, 8) + self.add_sprite("*", 71, 109, 8, 8) + + self.add_sprite("?", 1, 119, 8, 8) + self.add_sprite("-", 11, 119, 8, 8) + self.add_sprite("%", 1, 59, 8, 8) # index for upside down Y diff --git a/space_invaders.ttf b/space_invaders.ttf deleted file mode 100644 index c613ec774c608c39ad9ec6a186539bd0d8f6e9d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19380 zcmeHPZHQb~8GdJWCYxrPuO{hNzHXZsTTOP7^eYYdkq{dV6|6}qDus4-JJ}sGyR)60 zWH+_eD+r2IEmFiErL8~ID*d7050Qcug7k-I5NS*CBhWwoQ1FMvLYp0*=RN1W_uM-l zJ8cya_J%X>`FKB`_dV}9_uM;^m4b+@kyu6~`qb&l!P0kDy&a4=EAee#jfj7XojEb&~o*njLIHgACVhmpRbqXS~=nC zWf_wPeSH;Vr+j@>j>tJ*UoAW2r@np_5dF^A*T_fZbzdKo>k4`=1BR>R?!t|}f34h7 zIPL4}WW4YNU*9CR7JlUF0K2;IdrU{6L~r*O-thH;Y%8wu^`fjR?(y{zIf%Ql<1fkH z;u&8r%eBRC`}!)#zToSlvRJ(6>#OC+$Pr(^O0F4M^z}9Jz{uBqeN4tjF8lh`(kiX< z^|f+eDfabs@?hzbuWyo5<-M*BUpADVINqGQ(5g>Ox1+Nc8nu~fvpyRg+<##I>AC7; zEjl@SzWPY5HGir$b*@ouooLRsPq$m=CfiYVJll#q`vZ`yH)o>*<4)Z^Qy1u|&qvj$ z-GY^wYU^y&JQJNbeKJyp>E;{&v}>)|Y8z^*jp*Yu6Q7uFx91L5DrW#?UZIR@70lz4 z&6&iBZCOa$qlM~xG;yxpXh#e6_O!F0@@u@tg@uKTs=B$6A?|wnTCk?S|B3MyeRC%Pjg1%6>V3avI!KnZ#%$Coys!tw+#m z$-JBbox&<>ps$4$*~EAo5^c23L58{q$qyQfa@rs8HtT970=DrCx_w>fdgG}BcLcnV zqIAB{?-rsELH8Mao&f5TO3kBbjLe~j5ojxtS;({zD{@p3fk-}%_)W+sG^TBEAC?OK zol%Z9tfo;J4|<*EaYU;L`L3CWwhfA4v>kRAV4prsAm()-Vbm8;YXb>=F&|RlI|&A(Xr14$SRw%ty^f_&jQn=Isn3L~rQr zF~!6zrr$Ob%(rRCQJOprjGsk4>%nAByNkW)9*2z<^17nZ%zRp`p;Tb&0`5ZduuzfP zGy^M$a2>0H_9jr90(}&SxMt3xJ*z#BVSElKxbBGl^RVG&Y6g974i{BJ4Xf)I{@o+@ zLPJ9qQHsU$g{Pz}rJ~;Nc{}y@;oIalxa0UUpGb^D!O`Gud*bx{r{t)teW!@?9j=m| z0SfQ1KEgAfz$U6Fyw1RzzT7jA{F<7JV~qZr;Uvh*h8aE!Ee zr`5UGd9Cv*Mt(3^-$gOsh@Dsx6da=a9TZac z*@+8!GPx2nV8yu5S59Rh2Ofy>;nY(G8og?7XKF9&e9(#=2I4R*M$Lv%>o;CfoY7Qr zNpTn_X%3faPb4K7$6R&dVqD^^a<10njFUtuz}dG7RT@E_7HJo;%e48-5F#g!wOJ3_ z*6*q3bejf{(an_BRMrz^AWGMQuGI04haz1bB?Xy`h?ev9p($Jb~rCEU7 zR;~ndG8@c6Z@Y|>LCg@)HA=E7W8fTo#2rst?>`L7o4gj=I=MPVTu*8+i(`-m3y0 zZm7u|s~(l>(xbgDLXf76&)FPyj?u1O8R~F!uU#pVH<{D*a-D32{s9;Da~>^^Y-Kus z{TRj-2j~*2I`0;d*l~7X)$KMmFUo~Jj$gstm`0rf=W`!_sln_i)t26_{0Meb7r#}g zIDf-EYJkV&r%r04FJ8ZbvbG^lebsnFN;5zyW}`1MO3`i?2J~@E z3kb&m3ix5ucaD=-S|hgypNorHqa+E`#SOwWs((Je()R0%~}{j#e5Y zJNsb9(61hXET!}1CK4mqu7^6CC%YXIfOeLXv)u!H4w;)LJIlB;$+Gige%fB1r*KO3 z_n>hY4`3Lst#QF~2zR8exG?t_aZ=2$YBcKe@ICqmMg8zQ{eHZM-|_qOZxvN$_N;-K z$UOG3%ODLilH>VI#GM?1!3u~&Eor9qSijfdM1B(@yzE7depM$PHIjv8}-c2i?&(@A*EhtpEkK) zlj_-o{V2*EX{)G@W~C!Zr+jF2@{&{xGKYyc+09HR41MX^SeemG$K#s{p~NzNkc3bWL52Mb-Pc_6kF`|v&6d=t{~qsaWP!^T?!)50X_Vu z`lyCyZ*li0W+UnaNfvR_)`ZDl8Y}X;; zcjd4NQyLRc4F&q>x7sbC66VQzi%>eP)TKE}R$nLW-ih3vL4EWRpP;a;LH4=0Hm~3E zL!%;Y2Gts5-52~FSW-Wz4IJtV>QX{Uu`l};Cz&lsSsX$?ETj0&k@#T;l!X|ta7m$# z-V&Or-7&l)8?Xw@Kg)m0QcvDrdS%6NsD15C@~A5gl%O{CZ4~tomwVdTItX35GuryI zb!W5Nuix2CF}j%A34r~KF}y}iJu~WqulWGPNuD`w;8!WMbV)c_w+Dr5gzrXESty~E zURer7P6u}c)nio3=*Y(_qcg;&S||{wB_A6%62`=AyNseWH}lRpza~ETKBM{ELA1F} zm|4J}#n95Ou6O+nLvUZP9N8(mvhhrO&sBsMw1s(MR@7TRp5!}t+20*2pC2B(tv<&c z#4uK^UQ3WyUJm^Kg?Z$v%{&jn{xY`hebk`$3(!h$oOV}d7yI<`l(FKBA<8IjE|d6J zJTg>cL;+nRecvy}PGi?js0iqv%TIiQlXeH|Bf7*gxRW_g=2TZ?Z2Q-8`lwEIvJ(4y z0qf2RP<7$M-1BJ^d7qw3dahy9vRvL`Zes7dqp;eitxz>qh&q=EZ4e4qAva; z`8y3)uUXNR3_Z@0)3)-=#OQF3=@VVQPOv8O_izN3vNms=A>FI=zQ!6yR-T!J7b0Qz zRMr_Qi<=t_@;P=K2)yI;?UiRH7b!*svD4VBJTvKm4}oDkx=sq{(|!GgBB9 zTbH&T+19$oE?AezImzYT$}{D-|?Fq zgv%&)z8UyCBTA|^Pg!Tl=Qy-s;Q#WONn;T_GcEfj$)0nJ!f}mBKx22#%*_*?1akAH z;nn}M5jq%mf($y&SmnvI?Q+RP-psxmg~+CsRwMM*gDlyGTOM}loJw~d!dA) zp7dEJ&Klt8Ixxtt2`}{_H zh*@^4%*BLgywwb~n338k$2{mG5xai#Eu~$*v~`;y>~%f&Lq+Jlj}TUv^IcYxA<2(1 z{07tcybGK|T-4nw=H?;CEkhJP&3PFeLsO$*C}Z}L(^GA09X%Rn9Y?JPALNi;_|HLo z<3K5vWKEkM=3p3yVO9$C5WCH|p$>g#pZW)yKb4seM)Tio6A#xiK550}Wend4f3)Au zsIvin^%TU*+Iq!^=Dkwr1ht~L0Y-msp?7Sb0N~*m_cDi7b!vle+BD?J`zUbGWAJM{ zN@!YK*JCxk&%82>)XAc82TknwI&o&kje}9t5z}DQG>6o_)i&+mg=lVCXd+#88X;{Z<125pe44IdBpdysHMG99I_ZwE7KuqYfEimgp{W_IKgkBAPD_&cCC_!y-o*MW4#+vsOR@xsvG)L z!c%zS%*c)c7&Sp9T{H_eyh8YXbK|F!%g5@DD0-!CTBu$X{w#CdS zc|tyuLmx(%SDyB3_v<$~m!QZ?&4C{Km02{TE+WhjH`5qbB-yuQmhtd|=^e%_8z&4> zo~zAvMYd7!ndV56EB}4GFooxv+&UyS^Aza7gPtDiCO!8J%R^tG4pCzIphe$tv8q`x zj4f;$ud$X!Nf!&!BhQ1|(hP_QXK2u3Y_>H*qD zsVQ5yYP|*Cm>s^g{Z0-EFJxFJLe+<6f(U&c3u@L&Khz&&Xo806g-c_?=eQo3mA7bZ z)2s97JP)i96L(NeeF?{Mf_c* z2<0at>mk2EP|&yGpCTJEzVS&E*uUmJlxI->DzfPi3dT1>ZZq_3fzTGnZUNu5A4B=0 z$aP;qc?o}7<7$+z34d@UH~c~5M(DZmvdGr`C^5<TLpG1ub})5 ze{SPG6!af}?gP+$0JaX^j`B5>7g637xh+C@M&$Org8yEQ@-PZy?%aR^-a9{o@;J)# iC>K#6a~JyWYM{W6L(qK){C9)z?zizbx6pt1^?w6bBoul8 diff --git a/sprites/invader/invader-empty-frame.png b/sprites/invader/invader-empty-frame.png deleted file mode 100644 index f6268bc8d482e9c1e9c21b86b79736011244ad9c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~c!3HEhl+{lMQjEnx?oJHr&dIz4a`-)6978lF hCMQU+E^cUKWMJZ8V7#E2xDF`A;OXk;vd$@?2>`#G62t%i diff --git a/sprites/invader/invader-explode.png b/sprites/invader/invader-explode.png deleted file mode 100644 index 219e4a8c14b977637b2760db7dfa731aef2efe1e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~c!3HEhl+{lMQjEnx?oJHr&dIz4a*RD)978lF zCMO(V`uG3;e|dR%?E_0YRSt99beZPev2CWt<<3CwY1|t$4~ccS-0snTa`7p{S^4;d UrV-PPfZ7>6UHx3vIVCg!0QKi5x&QzG diff --git a/sprites/invader/invader-large-frame1.png b/sprites/invader/invader-large-frame1.png deleted file mode 100644 index 224853737f8e583c2d6bfd2b571b789e9be7ecb5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 139 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~c!3HEhl+{lMQjEnx?oJHr&dIz4a$G%K978lF zF6}$W$DqLBwD|x3^j#s&m!GPsJJz&L>`=MN>~O_+q5Y{P8>~b6ZBx>`KX-jzcKh!^ m=gBE;Pgg&ebxsLQ0F(GKzW@LL diff --git a/sprites/invader/invader-mid-frame2.png b/sprites/invader/invader-mid-frame2.png deleted file mode 100644 index 2649ef62e11cebd751ba655e02984beee26af480..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 141 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~c!3HEhl+{lMQjEnx?oJHr&dIz4a@;*#978lF zCMP&B{|AD8y*7UtZHMq4e`94$GeaYzq!sChmPy7c_WYOU+OVrZ#9k@-K#OmdKI;Vst0JxJVWdHyG diff --git a/sprites/invader/invader-small-frame2.png b/sprites/invader/invader-small-frame2.png deleted file mode 100644 index 4adda3ce40014d23827900d479fe9cc7bec6ccb4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 134 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~c!3HEhl+{lMQjEnx?oJHr&dIz4avVHe978lF zCMO(V{^!hSXWH|_WU)p{%8^xKs}=O3H%cCA@iyL+Bq2TfpoE&x7h}Hl38pNT0-TaQ gQ@AyZSf-~j?5?!Fr}MUbCD1GePgg&ebxsLQ01X)@00000 diff --git a/sprites/invader_bomb/bomb_exploding.png b/sprites/invader_bomb/bomb_exploding.png deleted file mode 100644 index a19be11cf6794621745c952fd69cdefe186af10d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 127 zcmeAS@N?(olHy`uVBq!ia0vp^Y(UJx0U~c5>$3n-jKx9jP7LeL$-D$|EInNuLpWyU z_8#P9FyLX@I(f(ckh~;&UrxpEg06xU8eCS!5!|9$T-&E*%DEc)@?2wOSkwLfcjil0 YK~WXchYJp>0S#gBboFyt=akR{0EK2E_W%F@ diff --git a/sprites/invader_bomb/bomb_exploding_base.png b/sprites/invader_bomb/bomb_exploding_base.png deleted file mode 100644 index 097f58d2749187510eba2bae6824e88dce559956..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 89 zcmeAS@N?(olHy`uVBq!ia0vp^Y(UJ&0V4mNy#E2jDGqXXVpw-h<|U9L?CIhd!Z9;B i0SJEl|NpYzhFX$DVMKbLh*2~7aQ(i#T< diff --git a/sprites/invader_bomb/explode-stem.png b/sprites/invader_bomb/explode-stem.png deleted file mode 100644 index c49966661fa75684841d837a965c858d92f3a02a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 93 zcmeAS@N?(olHy`uVBq!ia0vp^OhC-R!3HER>*_fHDaPU;cPEB*=VV?2IpUr!jv*W~ olmGnx|G%CM2-Mg#6#5w%#2Z<7JjGki0hKU#y85}Sb4q9e09nu#Qvd(} diff --git a/sprites/invader_bomb/plunger-frame1.png b/sprites/invader_bomb/plunger-frame1.png deleted file mode 100644 index e7aef2baa12014e7c69fabf78e990faedf4063bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^%s|Y+!3HF4-n-TSDaPU;cPEB*=VV?2ISQUGjv*W~ wlM{g8$N&HT>)C)njZMWtrCo5Dq&)+J#6=FK%jQWxfNB{$UHx3vIVCg!0BA=Um;e9( diff --git a/sprites/invader_bomb/plunger-frame2.png b/sprites/invader_bomb/plunger-frame2.png deleted file mode 100644 index da1491d4d8dc1731a690ed98ea7ab0eacb826b72..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105 zcmeAS@N?(olHy`uVBq!ia0vp^%s|Y+!3HF4-n-TSDaPU;cPEB*=VV?2Im(_cjv*W~ zlM{g8$N&HT>)C)njZLNDP-2T_BlDW70%itN2kt*B7^VdRl{0v{`njxgN@xNA;TRjj diff --git a/sprites/invader_bomb/plunger-frame3.png b/sprites/invader_bomb/plunger-frame3.png deleted file mode 100644 index 0b398a34bd7f167796c0c86359858257380e8bb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105 zcmeAS@N?(olHy`uVBq!ia0vp^%s|Y+!3HF4-n-TSDaPU;cPEB*=VV?2Im(_cjv*W~ zlM{g8$N&HT>)C)njZNhMhj1(Nno|K5I~W<}Y~=p4R9bB*P&tFAtDnm{r-UW|LETL1t6 diff --git a/sprites/invader_bomb/rolling-frame1.png b/sprites/invader_bomb/rolling-frame1.png deleted file mode 100644 index a8876c39fc72cb236a338b802a2ec7ab98856d37..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 99 zcmeAS@N?(olHy`uVBq!ia0vp^%s|Y+!3HF4-n-TSDaPU;cPEB*=VV?2IkKKEjv*W~ slM{g8$N&HT>)C)njZMYDke!u*p?y6I=hROV diff --git a/sprites/invader_bomb/rolling-frame2.png b/sprites/invader_bomb/rolling-frame2.png deleted file mode 100644 index 6fdde2ebfeb513cee983e5d9cfb7bbf8250b0858..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 108 zcmeAS@N?(olHy`uVBq!ia0vp^%s|Y+!3HF4-n-TSDaPU;cPEB*=VV?2IclCRjv*W~ zYfo?FWia44a-jKdd7YNZr6WsIn^hXiUTh68|7`O=N7ArL{Nrc28+)C)njZMYDke!u*p?y6I=hROV diff --git a/sprites/invader_bomb/rolling-frame4.png b/sprites/invader_bomb/rolling-frame4.png deleted file mode 100644 index d8dbdbded9f8cb93b775c8afcd1a10248a19da64..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 101 zcmeAS@N?(olHy`uVBq!ia0vp^%s|Y+!3HF4-n-TSDaPU;cPEB*=VV?2Ir5$^jv*W~ ylT#8B5`O&u|G%D1g87`|v_us@NnvI^1_sMo`KND=@JRucGI+ZBxvX)Axucpiu;IPx)BEm#1Et-XdGP2F3G?UkR$zcrbwzQP$h$>tDnm{r-UW|Y}Oj9 diff --git a/sprites/invader_bomb/squiggly-frame4.png b/sprites/invader_bomb/squiggly-frame4.png deleted file mode 100644 index d28019ea8492164606dc6422cb2bcdc1509520ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 94 zcmeAS@N?(olHy`uVBq!ia0vp^%s|Y+!3HF4-n-TSDaPU;cPEB*=VV?2ITD^Ojv*W~ plM@=6{sY1Bg9iefn&c!H7!qQnpWgVukOox4;OXk;vd$@?2><}e8odAj diff --git a/sprites/mothership/mothership-exploding.png b/sprites/mothership/mothership-exploding.png deleted file mode 100644 index 4a242a06156373f333389849a4263eb18bfa39f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^5%sGpWaG%11)0k MboFyt=akR{07TC`;s5{u diff --git a/sprites/mothership/mothership.png b/sprites/mothership/mothership.png deleted file mode 100644 index 008bab812c8dbe1ab8546d3b3465ee7211019b41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^5icK|HqX7KYN2~^}#(xZt`3+wb-AX zo)FftyE5dxg_#TEKjG;rwgL{aPow5ElrUdrNo=2X?%d~fp5`Bz43>$D|K4-d6KD~G Mr>mdKI;Vst04+Z{PXGV_ diff --git a/sprites/player/player-base-explodes-frame1.png b/sprites/player/player-base-explodes-frame1.png deleted file mode 100644 index af92f7673b9308b414f0f2790d483a2ae7d6f996..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 134 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~c!3HEhl+{lMQjEnx?oJHr&dIz4avVHe978lF zuJu37#h}1Z{O`a0dhezlHnF#hHcWSun6TvWyh72aq`8mV^z{pzzxUZ}-C4#Ez$cu{ huMzv!(sZ}C_>Yg?$7(B#B!Ffyc)I$ztaD0e0szJ#D=7c~ diff --git a/sprites/player/player-base-explodes-frame2.png b/sprites/player/player-base-explodes-frame2.png deleted file mode 100644 index 53bb93fc135c10b2cee94d84c73747e868a6fbd8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 139 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~c!3HEhl+{lMQjEnx?oJHr&dIz4a$G%K978lF zPMzS$$6&z168``H^S4WdjhyoaUR^JK05H8HB* mI)95HA?>7P=sL#M=lae+vlrZ)`dABSAcLo?pUXO@geCxKJ1wmM diff --git a/sprites/player/player-base.png b/sprites/player/player-base.png deleted file mode 100644 index aa77f37dbe51ea248a61db2dff525589c0c26f40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~c!3HEhl+{lMQjEnx?oJHr&dIz4a!fs4978lF zCMPH`i2T$3b6owr$C_vCJ^UM1r7AsPa$-0d;&jfH%|QGhuY#-bCPk6z#0R_#3?F6q Vyd0wyf`JAwc)I$ztaD0e0svn@A@u+N diff --git a/sprites/player/player-shield.png b/sprites/player/player-shield.png deleted file mode 100644 index eb80bc0cb3ca3dd68751649e16555500b17f2265..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^Vn8gw!3HFS-u9~jDaPU;cPEB*=VV?2IpLlzjv*Gk z$te!ZT>nM?E$$X%mNT$RaCx@4;Do8j?1u$xOWPJnpY!l_p2(ZNWZOc611{Xo?!0oQ z64nU^)jQ|?pUAlIOaJAgKX_E#mdKI;Vst051R*7XSbN diff --git a/sprites/player/player-shot-explodes.png b/sprites/player/player-shot-explodes.png deleted file mode 100644 index ce7b2fb301aa81f015b53f8c4ac690193109218c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 116 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|^gLZ0LnNjq z|M>s^e?6N@!x{I9hnG$eoUOuRU<3q@CbclC)l71Ube#2i<4+cb@|niB(#~B<0P1A$ MboFyt=akR{0PEExi2wiq diff --git a/sprites/player/player-shot.png b/sprites/player/player-shot.png deleted file mode 100644 index 6ad61c7bcf31197df88e9f950425b1f4418dba1f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90 zcmeAS@N?(olHy`uVBq!ia0vp^j6lr7!3HG#Z-iQb1d4;)ofy`glX(f`h Date: Sat, 4 Nov 2023 10:31:13 +0000 Subject: [PATCH 5/6] mvc controllers --- classes/Config.py | 1 + classes/Game_controller.py | 77 +++++- classes/config/Audio_config.py | 2 + classes/config/UI_config.py | 1 + classes/controllers/Audio_controller.py | 13 +- classes/controllers/Bomb_controller.py | 10 +- classes/controllers/Invader_controller.py | 16 ++ classes/controllers/Mothership_controller.py | 14 +- classes/controllers/Player_controller.py | 85 ++++-- .../controllers/Player_missile_controller.py | 68 ++--- classes/controllers/Scoreboard_controller.py | 2 + classes/controllers/Shield_controller.py | 39 +-- classes/controllers/UI_controller.py | 58 +++- .../controllers/UI_game_over_controller.py | 38 +++ classes/models/Player.py | 39 ++- classes/models/Player_missile.py | 30 +-- classes/models/Shield.py | 38 ++- images/sprite_sheet.png | Bin 2287 -> 2291 bytes lib/Controller.py | 4 + lib/Image_tint.py | 54 ++++ lib/Sprite_sheet.py | 2 + test.py | 248 ------------------ text.py | 56 ++++ 23 files changed, 487 insertions(+), 408 deletions(-) create mode 100644 classes/controllers/UI_game_over_controller.py create mode 100644 lib/Image_tint.py delete mode 100644 test.py create mode 100644 text.py diff --git a/classes/Config.py b/classes/Config.py index 066190e..4c7c7ee 100644 --- a/classes/Config.py +++ b/classes/Config.py @@ -65,6 +65,7 @@ class Config: "audio": { "mothership": "sounds/mothership.wav", "mothership_bonus": "sounds/mothership_bonus.wav", + "player_explodes": "sounds/player_destroyed.wav", }, } diff --git a/classes/Game_controller.py b/classes/Game_controller.py index 68c8db2..af6aaec 100644 --- a/classes/Game_controller.py +++ b/classes/Game_controller.py @@ -6,11 +6,27 @@ from lib.Controller import Controller from classes.config.Game_config import GameConfig +## +## need a state manager system to co-ordinate the screens +## state manager can poll the wipe position before changing state +## state manager can be built into base controller? +## + class GameController(Controller): + STARTING_LIVES = 1 + def __init__(self): super().__init__() config = GameConfig() + + # screen wipe settings + self.wipe_x = 0 # Initial position of the wipe effect + self.wipe_speed = 32 # Adjust this to control the speed of the wipe + self.is_wiping = False + + self.lives = self.STARTING_LIVES + self.has_extra_life = True self.play_delay_count = 120 self.top_left = config.get("top_left") self.original_screen_size = config.get("original_screen_size") @@ -25,6 +41,31 @@ def __init__(self): self.event_manager.add_listener( "escape_button_pressed", self.on_escape_button_pressed ) + self.event_manager.add_listener( + "player_explosion_complete", self.on_player_explosion_complete + ) + + self.event_manager.add_listener( + "extra_life_awarded", self.on_extra_life_awarded + ) + + self.event_manager.add_listener("game_over_animation_ended", self.on_begin_wipe) + + self.register_callback("get_lives_count", lambda: self.lives) + self.register_callback("get_extra_life", lambda: self.has_extra_life) + + # self.event_manager.add_listener("game_over_animation_ended", self.on_restart) + + def on_begin_wipe(self, data): + print("starting wipe...") + self.is_wiping = True + + def on_restart(self, data): + for controller in self.controllers: + if hasattr(controller, "game_restart") and callable( + controller.game_restart + ): + controller.game_restart() def debug_controllers(self): print("Ordered Controllers:") @@ -76,12 +117,23 @@ def load_controllers(self): ): controller_instance.game_ready() + def on_extra_life_awarded(self, data): + self.has_extra_life = False + self.lives += 1 + def on_escape_button_pressed(self, data): pygame.quit() + def on_player_explosion_complete(self, data): + self.lives -= 1 + if self.lives > 0: + self.play_delay_count = 120 + else: + self.event_manager.notify("game_ended") + # manage game over here + return + def update(self, events, dt): - # sys.exit() - # return if self.play_delay_count > 0: self.play_delay_count -= 1 if self.play_delay_count <= 0: @@ -89,6 +141,13 @@ def update(self, events, dt): # create a new game surface each frame game_surface = pygame.Surface(self.original_screen_size, pygame.SRCALPHA) + # game_surface.fill((0, 0, 0)) + + ## have an intermediate surface that all controllers get blitted to + ## have a background surface which gets the background image + ## have a foreground surface which gets the wipe blitted + ## then blit all of them onto main window + ## could just have the wipe render below the score board for controller_instance in self.controllers: # Call the update method if it exists on the controller @@ -99,9 +158,23 @@ def update(self, events, dt): canvas_item.draw(game_surface) # render the playing surface onto the main window + # if not self.is_wiping: self.window_surface.blit(self.scaled_image, self.top_left) + # scale the playing surface up to target size on main window self.window_surface.blit( pygame.transform.scale(game_surface, self.larger_screen_size), self.top_left, ) + + if self.is_wiping: + if self.wipe_x <= 224 * 4: + self.window_surface.blit( + self.scaled_image, (0, 160), (0, 160, self.wipe_x, 256 * 4) + ) + self.wipe_x += self.wipe_speed + else: + # possibly have a callback to poll the status instead of event callbacks + self.event_manager.notify("wipe_animation_complete") + self.is_wiping = False + self.wipe_x = 0 diff --git a/classes/config/Audio_config.py b/classes/config/Audio_config.py index e675d0b..7493c0e 100644 --- a/classes/config/Audio_config.py +++ b/classes/config/Audio_config.py @@ -6,4 +6,6 @@ class AudioConfig(BaseConfig): config_values = { "mothership": "sounds/mothership.wav", "mothership_bonus": "sounds/mothership_bonus.wav", + "player_explodes": "sounds/player_destroyed.wav", + "extra_life": "sounds/extra_life.wav", } diff --git a/classes/config/UI_config.py b/classes/config/UI_config.py index 313ed27..31df555 100644 --- a/classes/config/UI_config.py +++ b/classes/config/UI_config.py @@ -9,6 +9,7 @@ class UIConfig(BaseConfig): "hiscore_label_text": "HI-SCORE", "hiscore_label_position": (118, 16), "hiscore_value_position": (118, 32), + "game_over_position": (78, 48) # "tints": [ # {"position": (0, 0), "size": (224, 32), "color": (255, 255, 255)}, # {"position": (0, 32), "size": (224, 32), "color": (255, 0, 0)}, diff --git a/classes/controllers/Audio_controller.py b/classes/controllers/Audio_controller.py index d9b3b34..5ee78a7 100644 --- a/classes/controllers/Audio_controller.py +++ b/classes/controllers/Audio_controller.py @@ -9,20 +9,28 @@ class AudioController(Controller): def __init__(self): super().__init__() config = AudioConfig() + # print(config.config_values) self.mothership_sound = pygame.mixer.Sound(config.get("mothership")) - self.mothership_bonus_sound = pygame.mixer.Sound(config.get("mothership_bonus")) + self.player_explodes_sound = pygame.mixer.Sound(config.get("player_explodes")) + self.extra_life_sound = pygame.mixer.Sound(config.get("extra_life")) + self.event_manager.add_listener( "mothership_spawned", self.on_mothership_spawned ) self.event_manager.add_listener("mothership_hit", self.on_mothership_bonus) self.event_manager.add_listener("mothership_exit", self.on_mothership_exit) + self.event_manager.add_listener("player_explodes", self.on_player_explodes) + self.event_manager.add_listener("extra_life_awarded", self.on_extra_life) def on_mothership_spawned(self, data): pass # self.mothership_sound.play(-1) + def on_extra_life(self, data): + self.extra_life_sound.play() + def on_mothership_exit(self, data): self.mothership_sound.fadeout(1000) @@ -30,5 +38,8 @@ def on_mothership_bonus(self, data): self.mothership_sound.stop() self.mothership_bonus_sound.play() + def on_player_explodes(self, data): + self.player_explodes_sound.play() + def play_player_shot_sound(self): pass diff --git a/classes/controllers/Bomb_controller.py b/classes/controllers/Bomb_controller.py index 8dcd4d2..2a9aedb 100644 --- a/classes/controllers/Bomb_controller.py +++ b/classes/controllers/Bomb_controller.py @@ -19,9 +19,8 @@ def __init__(self): self.bomb_factory = BombFactory() self.bomb_container = BombContainer() - self.event_manager.add_listener( - "play_delay_complete", self.on_play_delay_complete - ) + self.event_manager.add_listener("play_delay_complete", self.on_player_ready) + self.event_manager.add_listener("player_explodes", self.on_player_explodes) self.register_callback("get_bombs", lambda: self.bomb_container.get_bombs()) @@ -32,7 +31,10 @@ def game_ready(self): "get_invaders_with_clear_path" ) - def on_play_delay_complete(self, data): + def on_player_explodes(self, data): + self.enabled = False + + def on_player_ready(self, data): self.enabled = True def update(self, events, dt): diff --git a/classes/controllers/Invader_controller.py b/classes/controllers/Invader_controller.py index 641624a..46e0fb4 100644 --- a/classes/controllers/Invader_controller.py +++ b/classes/controllers/Invader_controller.py @@ -15,6 +15,8 @@ def __init__(self): self.countdown = 0 self.event_manager.add_listener("invader_hit", self.on_invader_hit) + self.event_manager.add_listener("player_explodes", self.on_player_explodes) + self.event_manager.add_listener("play_delay_complete", self.on_player_ready) self.register_callback( "get_invaders", lambda: self.invader_container.get_invaders() @@ -34,6 +36,14 @@ def __init__(self): lambda: self.invader_container.get_invaders()[0].rect.y, ) + def game_restart(self): + invader_factory = InvaderFactory() + self.invader_generator = invader_factory.create_invader_swarm() + self.invader_container = InvaderContainer() + self.is_moving = False + self.swarm_complete = False + self.countdown = 0 + def on_invader_hit(self, invader): self.is_moving = False # pause invaders 1/4 second (60/15) @@ -41,6 +51,12 @@ def on_invader_hit(self, invader): invader.explode() self.event_manager.notify("points_awarded", invader.points) + def on_player_explodes(self, data): + self.is_moving = False + + def on_player_ready(self, data): + self.is_moving = True + def generate_next_invader(self): try: self.invader_container.add_invader(next(self.invader_generator)) diff --git a/classes/controllers/Mothership_controller.py b/classes/controllers/Mothership_controller.py index c22cd79..066fb7a 100644 --- a/classes/controllers/Mothership_controller.py +++ b/classes/controllers/Mothership_controller.py @@ -19,6 +19,7 @@ def __init__(self): self.points_table = self.config.get("points_table") self.mothership_image = self.sprite_sheet.get_sprite("mothership_frame") self.explode_image = self.sprite_sheet.get_sprite("explode_frame") + self.player_ready = False # set-up non-config related variables self.cycles_lapsed = 0 @@ -26,6 +27,8 @@ def __init__(self): self.shot_counter = 0 self.register_callback("mothership_is_exploding", self.mothership_is_exploding) + self.event_manager.add_listener("player_explodes", self.on_player_explodes) + self.event_manager.add_listener("play_delay_complete", self.on_player_ready) self.event_manager.add_listener( "fire_button_pressed", self.on_update_shot_counter @@ -57,7 +60,7 @@ def update_mothership(self, dt): self.event_manager.notify("mothership_exit") def update_spawn_logic(self): - if not self.check_invader_criteria(): + if not self.check_invader_criteria() or not self.check_player_criteria(): return False self.cycles_lapsed += 1 @@ -89,9 +92,18 @@ def reset_spawn_state(self): self.cycles_lapsed = 0 self.spawned = False + def on_player_explodes(self, data): + self.player_ready = False + + def on_player_ready(self, data): + self.player_ready = True + def on_update_shot_counter(self, data): self.shot_counter = (self.shot_counter + 1) % 16 + def check_player_criteria(self): + return self.player_ready + def check_invader_criteria(self): invader_count = self.get_callback("get_invader_count")() lowest_invader_y = self.get_callback("get_lowest_invader_y")() diff --git a/classes/controllers/Player_controller.py b/classes/controllers/Player_controller.py index 717a5c4..bc5918f 100644 --- a/classes/controllers/Player_controller.py +++ b/classes/controllers/Player_controller.py @@ -1,33 +1,33 @@ +import pygame from lib.Controller import Controller from classes.models.Player import Player -from lib.Sprite_sheet import PlayerSpriteSheet player_speed = 1 +# PLAY_DELAY_COMPLETE_EVENT = "play_delay_complete" +# LEFT_BUTTON_PRESSED_EVENT = "left_button_pressed" +# LEFT_BUTTON_RELEASED_EVENT = "left_button_released" +# RIGHT_BUTTON_PRESSED_EVENT = "right_button_pressed" +# RIGHT_BUTTON_RELEASED_EVENT = "right_button_released" +# PLAYER_EXPLOSION_COMPLETE_EVENT = "player_explosion_complete" + class PlayerController(Controller): def __init__(self): super().__init__() self.can_launch_missile = True self.enabled = False - sprite_sheet = PlayerSpriteSheet() + self.spawned = False + self.is_exploding = False - params = { - "player_sprite": sprite_sheet.get_sprite("player"), - "player_explode_sprites": [ - sprite_sheet.get_sprite("player_explode1"), - sprite_sheet.get_sprite("player_explode2"), - ], - "player_x_position": 10, - "player_y_position": 218, - } - - self.player = Player(params) + # player group has one player sprite + self.player_group_sprites = pygame.sprite.Group() self.left_key_pressed = False self.right_key_pressed = False self.register_callback("get_player", self.get_player) + self.register_callback("spawn_player", self.spawn_player) self.event_manager.add_listener( "play_delay_complete", self.on_play_delay_complete @@ -39,8 +39,18 @@ def __init__(self): "right_button_released", self.on_move_right_exit ) - def on_play_delay_complete(self, data): + def spawn_player(self): + self.spawned = True self.enabled = True + params = { + "player_x_position": 10, + "player_y_position": 219, + } + player = Player(params) + self.player_group_sprites.add(player) + + def on_play_delay_complete(self, data): + self.spawn_player() def on_move_left_exit(self, data): self.left_key_pressed = False @@ -55,17 +65,46 @@ def on_move_right(self, data): self.right_key_pressed = True def update(self, events, dt): - if self.enabled: - if self.left_key_pressed: - # self.player.rect.x -= player_speed * dt - self.player.rect.x -= player_speed - elif self.right_key_pressed: - # self.player.rect.x += player_speed * dt - self.player.rect.x += player_speed - return self.player + self.check_bomb_collisions() + if self.get_player(): + if self.enabled: + return self.update_player() + elif self.is_exploding: + return self.get_player().update() + + else: + if self.is_exploding: + print("end of explosion") + self.event_manager.notify("player_explosion_complete") + self.is_exploding = False + + def update_player(self): + player = self.get_player() + if self.left_key_pressed: + player.rect.x -= player_speed + elif self.right_key_pressed: + player.rect.x += player_speed + return player.update() def clamp(value, min_value, max_value): return max(min(value, max_value), min_value) def get_player(self): - return self.player + if self.player_group_sprites.sprites(): + return self.player_group_sprites.sprites()[0] + + def check_bomb_collisions(self): + if self.enabled: + bomb_sprites = self.callback("get_bombs") + + if bomb_sprites is not None: + for bomb_sprite in bomb_sprites: + if bomb_sprite.active and pygame.sprite.collide_mask( + self.get_player(), bomb_sprite + ): + bomb_sprite.explode() + print("hit") + self.is_exploding = True + self.enabled = False + self.get_player().explode() + self.event_manager.notify("player_explodes") diff --git a/classes/controllers/Player_missile_controller.py b/classes/controllers/Player_missile_controller.py index 88285b0..aef8cd7 100644 --- a/classes/controllers/Player_missile_controller.py +++ b/classes/controllers/Player_missile_controller.py @@ -1,14 +1,13 @@ from lib.Controller import Controller from classes.models.Player_missile import PlayerMissile -from lib.Sprite_sheet import PlayerSpriteSheet import pygame class PlayerMissileController(Controller): def __init__(self): super().__init__() - self.sprite_sheet = PlayerSpriteSheet() self.ready_flag = False + self.rendering_order = 1 # there will only ever be one sprite in this group self.missile_group = pygame.sprite.Group() @@ -19,78 +18,43 @@ def __init__(self): self.event_manager.add_listener("fire_button_pressed", self.on_fire_pressed) def game_ready(self): - # self.get_invaders_callback = self.get_callback("get_invaders") - self.get_player_callback = self.get_callback("get_player") - self.mothership_is_exploding = self.get_callback("mothership_is_exploding") + return def on_missile_ready(self, data): self.ready_flag = True def on_fire_pressed(self, data): - player = self.get_player_callback() if ( not self.missile_group and self.ready_flag - and not self.mothership_is_exploding() + and not self.callback("mothership_is_exploding") ): + player = self.callback("get_player") params = { - "missile_sprite": self.sprite_sheet.get_sprite("missile"), - "explode_sprite": self.sprite_sheet.get_sprite("missile_explode"), "player_x_position": player.rect.x, "player_y_position": player.rect.y, } self.missile_group.add(PlayerMissile(params)) - def check_collisions(self): + def check_invader_collisions(self): if self.missile_group: - collided_invaders = pygame.sprite.spritecollide( - self.missile_group.sprites()[0], self.get_invaders_callback(), False - ) - - if collided_invaders: - self.event_manager.notify("invader_hit", collided_invaders[0]) - self.ready_flag = False - return True + invaders = self.callback("get_invaders") + missile = self.get_player_missile() + for invader_sprite in invaders: + collision_area = pygame.sprite.collide_mask(invader_sprite, missile) + if collision_area is not None: + self.event_manager.notify("invader_hit", invader_sprite) + self.ready_flag = False + return True def update(self, events, dt): if self.missile_group: - if not self.check_collisions(): - return self.missile_group.sprites()[0].update() + if not self.check_invader_collisions(): + return self.get_player_missile().update() else: - self.missile_group.remove(self.missile_group.sprites()[0]) + self.missile_group.remove(self.get_player_missile()) # shields need access to player missile def get_player_missile(self): if self.missile_group: return self.missile_group.sprites()[0] - - # if self.player_missile: - # return self.player_missile.update() - # else: - # print("nothing..") - - # if not self.countdown > 0: - # if self.player_missile: - # self.check_collisions() - # self.player_missile.rect.y -= 5 - # if self.player_missile.rect.y <= 0: - # self.player_missile.explode() - # self.countdown = 15 - # else: - # print(self.countdown) - # self.countdown -= 1 - # if self.countdown <= 0: - # self.player_missile = None - # self.ready_flag = True - - # return self.player_missile - - # def explode(self): - # self.countdown -= 1 - # if self.countdown <= 0: - # self.player_missile = None - # self.ready_flag = True - - # def destroy_player_missile(self): - # self.player_missile = None - # self.ready_flag = True diff --git a/classes/controllers/Scoreboard_controller.py b/classes/controllers/Scoreboard_controller.py index 002dffc..d34b4f3 100644 --- a/classes/controllers/Scoreboard_controller.py +++ b/classes/controllers/Scoreboard_controller.py @@ -14,6 +14,8 @@ def __init__(self): def on_points_awarded(self, points): self.score += points + if self.score >= 500 and self.callback("get_extra_life"): + self.event_manager.notify("extra_life_awarded") def get_score(self): return str(self.score).zfill(5) diff --git a/classes/controllers/Shield_controller.py b/classes/controllers/Shield_controller.py index b965c0c..70b9211 100644 --- a/classes/controllers/Shield_controller.py +++ b/classes/controllers/Shield_controller.py @@ -7,15 +7,12 @@ class ShieldController(Controller): def __init__(self): super().__init__() - - # shield_factory = ShieldFactory() self.rendering_order = -1 self.shield_container = ShieldFactory().create_shields() - # self.missile_image_2x = pygame.image.load( - # "sprites/player/player-shot-double-height.png" - # ) - self.missile_image_2x = PlayerSpriteSheet().get_sprite("missile") + # place any code here that should run when controllers have loaded + def game_ready(self): + return def update(self, events, dt): self.check_bomb_collisions() @@ -24,30 +21,17 @@ def update(self, events, dt): return self.shield_container def check_missile_collision(self): - missile_callback = self.get_callback("get_player_missile") - missile = missile_callback() + missile = self.callback("get_player_missile") if missile is not None and missile.active: - _missile = pygame.sprite.Sprite() # Create an instance of the Sprite class - _missile.rect = missile.rect.copy() # Copy the rectangle - # because the missile moves up 4 pixels each cycle - # we need a sprite 2x height to ensure that sprite collision - # doesn't miss any pixels between position jumps - _missile.image = self.missile_image_2x # Copy the image - # _missile.rect.y -= 1 # Adjust the copied missile's position - for shield_sprite in self.shield_container: - if pygame.sprite.collide_mask(shield_sprite, _missile): - missile.explode(missile.rect.move(-4, -3)) - # self.rect.x -= 4 - # self.rect.y -= 3 - - shield_sprite.missile_damage(missile) - # self.event_manager.notify("missile_collision", shield_sprite) + collision_area = pygame.sprite.collide_mask(shield_sprite, missile) + if collision_area is not None: + shield_sprite.missile_collision(collision_area) + missile.explode((-4, 2)) def check_invader_collision(self): - invader_callback = self.get_callback("get_invaders") - invaders = invader_callback() + invaders = self.callback("get_invaders") if invaders: collisions = pygame.sprite.groupcollide( self.shield_container, invaders, False, False @@ -57,8 +41,7 @@ def check_invader_collision(self): shield_sprite.invader_damage(invader) def check_bomb_collisions(self): - bomb_callback = self.get_callback("get_bombs") - bomb_sprites = bomb_callback() + bomb_sprites = self.callback("get_bombs") if bomb_sprites is not None: for shield_sprite in self.shield_container: @@ -67,7 +50,7 @@ def check_bomb_collisions(self): shield_sprite, bomb_sprite ): bomb_sprite.explode() - shield_sprite.bomb_damage(bomb_sprite) + shield_sprite.bomb_collision(bomb_sprite) # if self.get_bombs_callback: # bomb_sprites = self.get_bombs_callback() diff --git a/classes/controllers/UI_controller.py b/classes/controllers/UI_controller.py index 33ba0e5..b6f23f3 100644 --- a/classes/controllers/UI_controller.py +++ b/classes/controllers/UI_controller.py @@ -2,22 +2,61 @@ from lib.Controller import Controller from classes.config.UI_config import UIConfig from lib.Sprite_sheet import FontSpriteSheet +from lib.Sprite_sheet import PlayerSpriteSheet class UIController(Controller): + CANVAS_HEIGHT_PLAYER_LIVES = 8 + CANVAS_HEIGHT = 256 + CANVAS_WIDTH = 224 + def __init__(self): super().__init__() self.config = UIConfig() - self.canvas_width = 224 - self.canvas_height = 256 + self.canvas_width = self.CANVAS_WIDTH + self.canvas_height = self.CANVAS_HEIGHT self.canvas = pygame.Surface( (self.canvas_width, self.canvas_height), pygame.SRCALPHA ) self.sprite_sheet = FontSpriteSheet() + self.player_sprite_sheet = PlayerSpriteSheet() self.register_callback("get_score_text", self.create_text_surface) - def game_ready(self): - self.get_score_callback = self.get_callback("get_score") + def text_generator(self, text_to_display): + time_between_chars = 10 # Delay frames between each character + char_index = 0 + frame_count = 0 + current_text = "" + + while char_index < len(text_to_display): + if frame_count >= time_between_chars: + current_text = text_to_display[: char_index + 1] + yield current_text + char_index += 1 + frame_count = 0 + else: + yield current_text # Return the progressively increasing text during the delay + frame_count += 1 + + def draw_lives(self): + lives = self.callback("get_lives_count") + player_base = self.player_sprite_sheet.get_sprite("player") + # create a canvas the size of players + if lives > 0: + lives_canvas = pygame.Surface( + (17 * lives, self.CANVAS_HEIGHT_PLAYER_LIVES), pygame.SRCALPHA + ) + for i in range(lives - 1): + lives_canvas.blit(player_base, (i * 16 + 12, 0)) + else: + lives_canvas = pygame.Surface( + (17, self.CANVAS_HEIGHT_PLAYER_LIVES), pygame.SRCALPHA + ) + + lives_remaining_canvas = self.create_text_surface(str(lives)) + lives_canvas.blit(lives_remaining_canvas, (0, 0)) + + return lives_canvas def draw(self, surface): surface.blit(self.canvas, (0, 0)) # blit the canvas onto the game surface @@ -25,8 +64,12 @@ def draw(self, surface): def update(self, events, dt): # clear the canvas between each draw self.canvas.fill((0, 0, 0, 0)) + # get the score value using the callback to the scoreboard controller - score = str(self.get_score_callback()) + score = self.callback("get_score") + + lives_canvas = self.draw_lives() + self.canvas.blit(lives_canvas, (1, 242)) # position SCORE text at position in config self.canvas.blit( @@ -57,7 +100,8 @@ def create_text_surface(self, text): text_surface.fill((0, 0, 0, 0)) for idx, letter in enumerate(text): - char_image = self.sprite_sheet.get_sprite(letter) - text_surface.blit(char_image, (idx * 8, 0)) + if not letter == " ": + char_image = self.sprite_sheet.get_sprite(letter) + text_surface.blit(char_image, (idx * 8, 0)) return text_surface diff --git a/classes/controllers/UI_game_over_controller.py b/classes/controllers/UI_game_over_controller.py new file mode 100644 index 0000000..a8bf416 --- /dev/null +++ b/classes/controllers/UI_game_over_controller.py @@ -0,0 +1,38 @@ +from classes.controllers.UI_controller import UIController + + +class UIGameOverController(UIController): + def __init__(self): + super().__init__() + + self.game_over_message_position = (78, 48) + self.game_over_text = "GAME OVER" + + self.game_over_text_iterator = self.text_generator(self.game_over_text) + self.game_over_text_running = False + + self.event_manager.add_listener("game_ended", self.on_game_over) + self.pause = 0 + + def on_game_over(self, data): + print("on game over") + self.game_over_text_running = True + + def update(self, events, dt): + self.canvas.fill((0, 0, 0, 0)) + + if self.game_over_text_running: + try: + current_text = next(self.game_over_text_iterator) + except StopIteration: + current_text = self.game_over_text + + self.pause += 1 + if self.pause == 180: + self.event_manager.notify("game_over_animation_ended") + self.game_over_text_running = False + + text_surface = self.create_text_surface(current_text) + self.canvas.blit(text_surface, self.config.get("game_over_position")) + + return self diff --git a/classes/models/Player.py b/classes/models/Player.py index 0b13c78..a9366ba 100644 --- a/classes/models/Player.py +++ b/classes/models/Player.py @@ -1,15 +1,52 @@ from lib.Game_sprite import GameSprite +from lib.Sprite_sheet import PlayerSpriteSheet class Player(GameSprite): + ANIMATION_FRAME_THRESHOLD_LOW = 5 + ANIMATION_FRAME_THRESHOLD_HIGH = 10 + MAX_ANIMATION_COUNT = 6 + def __init__(self, params): super().__init__() - self.image = params.get("player_sprite") + self.explosion_frame_number = 0 + self.explosion_animation_count = 0 + + self.is_exploding = False + self.sprite_sheet = PlayerSpriteSheet() + self.image = self.sprite_sheet.get_sprite("player") + self.exploding_images = [ + self.sprite_sheet.get_sprite("player_explode1"), + self.sprite_sheet.get_sprite("player_explode2"), + ] self.rect = self.image.get_rect( x=params.get("player_x_position"), y=params.get("player_y_position") ) + def update(self): + if self.is_exploding: + return self.update_exploding() + else: + return self + + def explode(self): + self.is_exploding = True + self.image = self.exploding_images[0] + + def update_exploding(self): + if self.explosion_animation_count < self.MAX_ANIMATION_COUNT: + self.explosion_frame_number += 1 + if self.explosion_frame_number == self.ANIMATION_FRAME_THRESHOLD_LOW: + self.image = self.exploding_images[1] + elif self.explosion_frame_number == self.ANIMATION_FRAME_THRESHOLD_HIGH: + self.image = self.exploding_images[0] + self.explosion_frame_number = 0 + self.explosion_animation_count += 1 + return self + else: + self.kill() + # used by BombController def get_rect(self): return self.rect diff --git a/classes/models/Player_missile.py b/classes/models/Player_missile.py index 138ab91..43ff13a 100644 --- a/classes/models/Player_missile.py +++ b/classes/models/Player_missile.py @@ -1,13 +1,15 @@ from lib.Game_sprite import GameSprite +from lib.Sprite_sheet import PlayerSpriteSheet class PlayerMissile(GameSprite): def __init__(self, params): super().__init__() - self.delay = 1 - self.image = params.get("missile_sprite") - self.explode_frame = params.get("explode_sprite") + sprite_sheet = PlayerSpriteSheet() + self.image = sprite_sheet.get_sprite("missile") + self.explode_frame = sprite_sheet.get_sprite("missile_explode") + self.countdown = 0 self.active = True self.rect = self.image.get_rect( @@ -15,25 +17,20 @@ def __init__(self, params): ) def draw(self, surface): + # surface.blit(self.image, self.rect) surface.blit(self.modify_pixel_colors(self.image), self.rect) def remove(self): self.kill() - def explode(self, position_rect=None): + def explode(self, offset_rect=None): if self.active: self.image = self.explode_frame - if position_rect: - print("updated rect") - self.rect.x = position_rect[0] - self.rect.y = position_rect[1] - - # print(self.rect) - # if position_rect - # self.rect.x -= 4 - # self.rect.y -= 3 + if offset_rect: + self.rect = self.rect.move(offset_rect) + self.active = False - self.countdown = 30 + self.countdown = 15 def update(self): if self.countdown > 0: @@ -41,11 +38,8 @@ def update(self): if self.countdown <= 0: self.kill() else: - # if self.delay <= 0: - self.delay = 1 self.rect.y -= 4 # Move the missile vertically upwards if self.rect.y <= 42: self.explode(()) - # else: - # self.delay -= 1 + return self diff --git a/classes/models/Shield.py b/classes/models/Shield.py index 624c66c..d4523ee 100644 --- a/classes/models/Shield.py +++ b/classes/models/Shield.py @@ -1,6 +1,8 @@ +import random import pygame from lib.Game_sprite import GameSprite +from lib.Sprite_sheet import PlayerSpriteSheet class Shield(GameSprite): @@ -16,38 +18,28 @@ def __init__(self, x, y, image): # used in collision detection self.modify_pixel_colors(self.image) self.mask = pygame.mask.from_surface(self.image) + self.missile_explode_frame = PlayerSpriteSheet().get_sprite("missile_explode") - def missile_damage(self, missile_sprite): - shield_rect = self.rect - - global_position = missile_sprite.rect.topleft - # print(global_position) - new_y = global_position[1] - 0 - new_x = global_position[0] - 0 - - # Create a new tuple with the modified y-position - global_position = (new_x, new_y) + def missile_collision(self, collision_area): + modified_shield_surface = self.image.copy() - # print(global_position) - # Convert global position to local position inside the shield - local_position = ( - global_position[0] - shield_rect.x, - global_position[1] - shield_rect.y, - ) + collision_rect = pygame.Rect(collision_area[0], collision_area[1], 0, 0) + # update collision to align with preferred shield damage area + collision_rect = collision_rect.move((-4, -2)) - modified_shield_surface = self.image.copy() modified_shield_surface.blit( - missile_sprite.explode_frame, - # (local_position[0], 0 - y_adjust), - (local_position[0], local_position[1]), + self.missile_explode_frame, + collision_rect, special_flags=pygame.BLEND_RGBA_SUB, ) + self.image = modified_shield_surface - # update the sprite mask so future collisions - # use the mask rather than a basic rect + # pygame.time.delay(1000) + + # update the sprite mask for future collisions self.mask = pygame.mask.from_surface(modified_shield_surface) - def bomb_damage(self, bomb_sprite): + def bomb_collision(self, bomb_sprite): shield_rect = self.rect global_position = bomb_sprite.rect.topleft diff --git a/images/sprite_sheet.png b/images/sprite_sheet.png index 8f9341e737fdc2b40c8b1e2f6bf7978a90f4d5df..e3727c95fe98c75d620a1741f7ef383ffc083480 100644 GIT binary patch delta 2266 zcmV<02qpLL5%Up{F@LQ|L_t(|ob6rhmg_1AHM8!6B~PBdnfK3e4y7O)C5iU9XLU`R z5JY!G!F+7{zUQ+ESp|!U+T5Uw$Xr);vp`j zirb`Q@u1dKLeeuV2&n5V<x!GB~(O2}mp7CnNS!TiXOUD4RH5jZV*3yZv|vB;Yk=Di9Cq}*r_yYaS?o|1hFG5wA-uGGt zme7Kh&l3VO;v=_-_K1}1hO)v11d^-Mj<1~*R=s*%B|Uymdz+5rv!OXXX36v3K+kmK ztVa_Byk{Z_gl|E(+mzxvNsc#kZ9ng^bez-1GD%;pta`1+MEG@^(!@dgUI?l+IO4`@ zu2j+&Jz04QmrPg?cZKv&t2cSdrGL7l87HEDG^`BE#8?@Yi2-F;tPBc==I*{-&)pC& zgFp_{q>NySK=!qrbBeK=;5&&ACElt)Z)(J-DWY|GLFm9AXz#?YkzSX)wK~w zdeL~V#~w*fuRT{md=5*EKuwNskI+@zA96xjx)FOUT|M?(fv^dSRw2y|X^?tiOM{JO z&lOl1mWi=4EE6L%!}71nJq2+k-I^WJ->9r6hw9T}vsO8@Y~CK;G4o2ik;eyT?I5Hw zo=UgK)qu#2L_#avp+YNWZ-1x}(X^6|xWg-emK$x~(;GPlXc%?LN=ndw@%VqE916{x+IO?yR+bgXtw>B_JKq1@v@ z$>}9elP_2qmS9Nlz(cRb_P)MtWmp2_D7^2>L!jNc^s$~-fGVyn_kXm=q2=_JTlQJ? z%?vdgwMP|8+t2g-*lxYPjpJ>?f@D`(5T!G6DJD7tWW9o|XPZXgvd~+$G)D!5{P{BXJuGINT$Hxe5%#g+gZEW$3dB_-r z==0czl-;85F1X`7Zn%|U(S9R3TJ)jzdCDGzERNqXuO*Y-)}b@J3l zs~^=fDnGTIw6l1}Sx}?%V`+z3dNaYZPiqIQk7P)sZKI$XPk-wyZvnKUXh$QuJmuA7 z`3Y1EhK|5d5sfFgN_3=HdRldDf6zW7$7|)yXt@snv5 zLyE?ujgj6X)XsFNENdU{ShS-N*I2gCVzfea&MO9>?>)-t+`^&zE%@Tcv*6+Fp~ut~ zRWl%tJZdaH@_#%duqKP>s?ELhy)_Co4?^qUZQBTJqsKA}oobx77@M!ouzWkNP@!c$ zkmeHayov0aX|9SwHJ;RIEe7N(JC81JLf}aQp6B`Y`8bZZZO3t_a(sK)X7SHtSe}9# zhZ;uOhZ-)W8xqsfu`PN_e5ZXL1>X6R(Q5FKSwl3Q6n{i}18YPU*)NS+WZP&UM)4Rf zrHb36#^OP(tAwOySP-b|E#=d|n#zoTWB@wq=>FQML=36sHl+oCXHg&-oWH04YkDMu zmHHq$?m#ktG{0%?lW#}d?8>lAOf}A1;CSb&UXJ7Vn_($#sf@()Jb&F0&3tgVk`2i@ z=Q7kaIe%oE6z}xtE8DofQfKONXY)coDamu0wm6ZtM+V3(tJ)gUyvVnWwBYgZlTx3d zdJLy!Up&WJNOhw|EA^kzfWM(atD(?_ONQzhW!_O^B?`Jn`nKKsO2l|QhC&P&?GCwK z4b5$i5MK0h$(+~Y4pDmDW^E+S!Xz-r`Ojn<6Z za)OkTlDhKlk(cwmue35OVLTC}uX|V~X3F%KcCSo}F3HW#;*Gvy40ng63~59zbk>#hAYSL|*7EjaUm2E}fif&sz>Iv01L6j)ObuE`sz-YEg3IUp z_c@E1Fh`!WLi8eaJ`(8jV{7rEHrH}JQfwJ9zh?3mp~L!1>88XqKLM-`(+oyCyniLM zpyl&~z>Iw4HqjB0lHE~OxPTzJI_>z{Nny3C*KN|{_q4a^NWMBWr^hULJ{stmj-2&q zf`E@qB!TcPgu6{CzLVs5N7wfA5liPeZ7!4a)yk^3YD~iK+msd#+WTUlT8ATUyyi+J zebI}Rr?_Ong19SWgj%D?Q!dpdt$#QX{iCrmEEBUbEE5A|SgZ^RM{{@IuIFyV+aQnw zHMy3hdTqW#%j(gC=o&Eu33Z!R=iQ;l>SRxSXk6N(=PJ64M}uU^rFd3zEqdt6k%s`@ z`qXswwChT`j920-B&I`jwdmoo8=sFn^{K_3!@JE=ajSA%jt0rn8Cr|esDG}FAn8Tp zy`K6=dV1?~72@}>)C{P}@$C`1iu*%OC`&g|A4^wHeXbyE!J;)tb3+=^NNnk_(du&r zE5kA|E5kA|p&6EcRqiRoE9utikbXvGH91tD7Mr!op=I;_@Xnc6@@70fIcoSIq1DX9a3M%clKAjdZMbPwC3A1aY3{hjDt1 zr^y#qh9wy3H}KHAvAsXvwlXXMauo0Hiv*Wr-SrqXzMAx9RPWD}rH9*-w;v7h-PH zn!LSFA+J8|4IXj~pwK6P0oUhldsUgGS z$vCv=>&=`zCHDj$w?PzYl|N_DK+f zT$S_frRm97>gFst#0`&5G2Ccv_%I%NmRXH?GOL2RIm9SbM}J-Rp-_QW%xgeNF4}E+ z^m*zp>7Ip^VF?1dsu0M&elvD%R)!@&E~Vel>o$&^%z1o{(B=$jZqViy&zy(MQHVZI z{gAR-^xcI!&f^ugGA!C>L`RE0w0@qlM==)Xcg}0cBzWX%`w<;29f*bsh@OJC4p;K= ztilzBi#&5nCx3ceY)`#5hN}00+-7rn>?y;c`idOtu18FNSv1|l;_bi)qFi)=j;8Xt zN8b~4C#F-98}7-7c+y#s`&kEL^M%OMu){s6Y5y3%)s;ax^~j#!(T0 zw-+;3&H2_>O3L@JR4W;rVL|1*b&Oj2d+T>6;`gwi+<)B=Zz+(Ep-Lh%3}zrrSi_1JaQmcf;j08Lt7YHq`1N$zJVg zHs?%z=_6iV(9w)$T9DC%>f!&n-2T8XUyApzl+QhiQGMPr5d%h!5UyvG#Y##m!!k81 z!!j|KWKUQY_n?03-$=fFA>+OQX)z9O_pp@r%W!kh{RQDEEf@O~-M9WNo}=!uFf k{51b^<>Gl(hGk;@2iy{E!#_SH?f?J)07*qoM6N<$f~K;TJ^%m! diff --git a/lib/Controller.py b/lib/Controller.py index 0d1cbf7..b57489d 100644 --- a/lib/Controller.py +++ b/lib/Controller.py @@ -23,6 +23,10 @@ def register_callback(cls, key, callback, name=None): def get_callback(cls, key): return cls.callbacks.get(key) + @classmethod + def callback(cls, key): + return cls.callbacks.get(key)() + @classmethod def debug_callbacks(cls): print("Callbacks:") diff --git a/lib/Image_tint.py b/lib/Image_tint.py new file mode 100644 index 0000000..8586169 --- /dev/null +++ b/lib/Image_tint.py @@ -0,0 +1,54 @@ +import pygame + + +class Image_tint: + def modify_pixel_colors(self, image): + if isinstance(image, pygame.Surface): + return self.apply_image_tints(image) + else: + print("not an image being converted") + + def apply_image_tints(self, image): + area_colors = [0, 1, 0, 3, 0, 3, 0] # Corresponding colors for each area + areas = [ + {"position": (0, 0), "size": (224, 32)}, + {"position": (0, 32), "size": (224, 32)}, + {"position": (0, 64), "size": (224, 120)}, + {"position": (0, 184), "size": (224, 56)}, + {"position": (0, 240), "size": (24, 16)}, + {"position": (24, 240), "size": (112, 16)}, + {"position": (136, 240), "size": (88, 16)}, + ] + + # banding colours + colors = [ + (255, 255, 255), # White + (255, 0, 0), # Red + (255, 255, 255), # White + (0, 255, 0), # Green + (255, 255, 255), # White + (0, 255, 0), # Green + (255, 255, 255), # White + ] + + for y in range(image.get_height()): + for x in range(image.get_width()): + pixel_color = image.get_at((x, y)) + pixel_x, pixel_y = ( + self.rect.x + x, + self.rect.y + y, + ) # Pixel position in the sprite's coordinate system + + for i, area in enumerate(areas): + area_rect = pygame.Rect(area["position"], area["size"]) + if area_rect.collidepoint(pixel_x, pixel_y): + color_index = area_colors[i] + new_color = colors[color_index] + pixel_color.r, pixel_color.g, pixel_color.b = new_color + + image.set_at((x, y), pixel_color) + + return image + + +__all__ = ["modify_pixel_colors"] diff --git a/lib/Sprite_sheet.py b/lib/Sprite_sheet.py index 208c3d3..7ffd9ae 100644 --- a/lib/Sprite_sheet.py +++ b/lib/Sprite_sheet.py @@ -80,6 +80,8 @@ def __init__(self): self.add_sprite("player_explode1", 19, 49, 16, 8) self.add_sprite("player_explode2", 37, 49, 16, 8) self.add_sprite("missile", 55, 49, 1, 8) + + self.add_sprite("missile_2x", 68, 49, 1, 8) self.add_sprite("missile_explode", 58, 49, 8, 8) diff --git a/test.py b/test.py deleted file mode 100644 index bad9ddf..0000000 --- a/test.py +++ /dev/null @@ -1,248 +0,0 @@ -import pygame - - -class SpriteSheet: - def __init__(self, image_path): - self.sprite_sheet = pygame.image.load(image_path) - self.sprite_lookup = {} - - def add_sprite(self, name, x, y, width, height): - self.sprite_lookup[name] = (x, y, width, height) - - def get_sprite(self, name): - sprite_rect = self.sprite_lookup.get(name) - if sprite_rect: - x, y, width, height = sprite_rect - sprite = pygame.Surface((width, height), pygame.SRCALPHA) - sprite.blit(self.sprite_sheet, (0, 0), (x, y, width, height)) - return sprite - else: - raise ValueError(f"Sprite with name '{name}' not found in the spritesheet.") - - -# Example usage: -if __name__ == "__main__": - pygame.init() - screen = pygame.display.set_mode((400, 400)) - - # Create a SpriteSheet instance - sprite_sheet = SpriteSheet("images/font_spritesheet.png") - - # Add sprites to the sprite lookup table with their respective positions and sizes - sprite_sheet.add_sprite("sprite1", 1, 1, 8, 8) - sprite_sheet.add_sprite("sprite2", 11, 1, 8, 8) - sprite_sheet.add_sprite("sprite3", 21, 1, 8, 8) - # Add more sprites as needed - - running = True - while running: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - - screen.fill((0, 0, 0)) - sprite = sprite_sheet.get_sprite("sprite1") - screen.blit(sprite, (100, 100)) - sprite = sprite_sheet.get_sprite("sprite2") - screen.blit(sprite, (64, 100)) - pygame.display.flip() - - pygame.quit() - - -# import pygame -# import sys - -# # Initialize Pygame -# pygame.init() - -# # Define the screen dimensions -# screen_width = 224 -# screen_height = 256 -# screen = pygame.display.set_mode((screen_width, screen_height)) - -# # Define the colors based on your provided areas -# colors = [ -# (255, 255, 255), # White -# (255, 0, 0), # Red -# (255, 255, 255), # White -# (0, 255, 0), # Green -# (255, 255, 255), # White -# (0, 255, 0), # Green -# (255, 255, 255), # White -# ] - -# # Define the positions and sizes of the areas -# areas = [ -# {"position": (0, 0), "size": (224, 32)}, -# {"position": (0, 32), "size": (224, 32)}, -# {"position": (0, 64), "size": (224, 120)}, -# {"position": (0, 184), "size": (224, 56)}, -# {"position": (0, 240), "size": (24, 16)}, -# {"position": (24, 240), "size": (112, 16)}, -# {"position": (136, 240), "size": (88, 16)}, -# ] - -# # Main loop -# running = True -# while running: -# for event in pygame.event.get(): -# if event.type == pygame.QUIT: -# running = False - -# # Fill the screen with the specified colors -# for i, area in enumerate(areas): -# color = colors[i] -# pygame.draw.rect(screen, color, pygame.Rect(area["position"], area["size"])) - -# pygame.display.flip() - -# # Quit Pygame -# pygame.quit() -# sys.exit() - -# import pygame -# import sys - -# # Initialize Pygame -# pygame.init() - -# # Define the screen dimensions -# screen_width = 224 -# screen_height = 256 -# screen = pygame.display.set_mode((screen_width, screen_height)) - - -# # Define the colors based on your provided areas with reduced alpha value (transparency) -# colors = [ -# (255, 255, 255), # White -# (255, 0, 0), # Red -# (255, 255, 255), # White -# (0, 255, 0), # Green -# (255, 255, 255), # White -# (0, 255, 0), # Green -# (255, 255, 255), # White -# ] - -# # Define the alpha values for the colors -# alpha_values = [255, 100, 255, 100, 255, 100, 255] - -# # Create semi-transparent surfaces for the colored rectangles -# colored_rect_surfaces = [ -# pygame.Surface((224, 32), pygame.SRCALPHA), -# pygame.Surface((224, 32), pygame.SRCALPHA), -# pygame.Surface((224, 120), pygame.SRCALPHA), -# pygame.Surface((224, 56), pygame.SRCALPHA), -# pygame.Surface((24, 16), pygame.SRCALPHA), -# pygame.Surface((112, 16), pygame.SRCALPHA), -# pygame.Surface((88, 16), pygame.SRCALPHA), -# ] - -# # Define the positions and sizes of the areas -# areas = [ -# {"position": (0, 0), "size": (224, 32)}, -# {"position": (0, 32), "size": (224, 32)}, -# {"position": (0, 64), "size": (224, 120)}, -# {"position": (0, 184), "size": (224, 56)}, -# {"position": (0, 240), "size": (24, 16)}, -# {"position": (24, 240), "size": (112, 16)}, -# {"position": (136, 240), "size": (88, 16)}, -# ] - -# # Fill the colored rectangles with the specified colors and alpha values -# for i, surface in enumerate(colored_rect_surfaces): -# color = colors[i] -# alpha = alpha_values[i] -# # surface.fill((color[0], color[1], color[2], alpha)) - - -# # Create a sprite class -# class PlayerSprite(pygame.sprite.Sprite): -# def __init__(self): -# super().__init__() -# self.image = pygame.Surface((32, 32), pygame.SRCALPHA) -# self.rect = self.image.get_rect() -# self.rect.center = (screen_width // 2, screen_height // 2) -# self.color = (255, 255, 255) -# self.alpha = 255 - -# def modify_pixel_colors(self): -# area_colors = [0, 1, 0, 3, 0, 3, 0] # Corresponding colors for each area -# # for frame in self.modified_frames: -# for y in range(self.image.get_height()): -# for x in range(self.image.get_width()): -# pixel_color = self.image.get_at((x, y)) -# pixel_x, pixel_y = ( -# self.rect.x + x, -# self.rect.y + y, -# ) # Pixel position in the sprite's coordinate system - -# for i, area in enumerate(areas): -# area_rect = pygame.Rect(area["position"], area["size"]) -# if area_rect.collidepoint(pixel_x, pixel_y): -# color_index = area_colors[i] -# new_color = colors[color_index] -# pixel_color.r, pixel_color.g, pixel_color.b = new_color -# pixel_color.a = 127 - -# self.image.set_at((x, y), pixel_color) - -# def update_color(self): -# for i, area in enumerate(areas): -# area_rect = pygame.Rect(area["position"], area["size"]) -# if area_rect.colliderect(self.rect): -# self.color, self.alpha = colors[i], alpha_values[i] -# return -# self.color, self.alpha = (0, 0, 0), 255 # Default color (black with full alpha) - -# def update(self): -# self.modify_pixel_colors() - - -# # Create a sprite group -# all_sprites = pygame.sprite.Group() -# player_sprite = PlayerSprite() -# all_sprites.add(player_sprite) - -# # Set the desired frame rate (60 fps) -# clock = pygame.time.Clock() -# desired_fps = 60 - -# # Main loop -# running = True -# while running: -# for event in pygame.event.get(): -# if event.type == pygame.QUIT: -# running = False - -# # Check for key presses to move the sprite -# keys = pygame.key.get_pressed() -# if keys[pygame.K_LEFT]: -# player_sprite.rect.x -= 1 -# if keys[pygame.K_RIGHT]: -# player_sprite.rect.x += 1 -# if keys[pygame.K_UP]: -# player_sprite.rect.y -= 1 -# if keys[pygame.K_DOWN]: -# player_sprite.rect.y += 1 - -# # Clear the screen -# screen.fill((0, 0, 0)) # Fill the entire screen with white - -# # Draw the semi-transparent colored rectangles -# for i, area in enumerate(areas): -# surface = colored_rect_surfaces[i] -# screen.blit(surface, (area["position"][0], area["position"][1])) - -# # Update and draw the player sprite -# all_sprites.update() -# all_sprites.draw(screen) - -# pygame.display.flip() - -# # Limit the frame rate to desired_fps (60 fps) -# clock.tick(desired_fps) - -# # Quit Pygame -# pygame.quit() -# sys.exit() diff --git a/text.py b/text.py new file mode 100644 index 0000000..c8b8207 --- /dev/null +++ b/text.py @@ -0,0 +1,56 @@ +import pygame +import sys +import time + + +class GameOverAnimation: + def __init__(self): + pygame.init() + self.SCREEN_WIDTH, self.SCREEN_HEIGHT = 800, 600 + self.BACKGROUND_COLOR = (255, 255, 255) + self.TEXT_COLOR = (0, 0, 0) + self.FONT_SIZE = 36 + self.FONT_NAME = "Arial" + self.game_over_text = "GAME OVER" + self.font = pygame.font.Font(None, self.FONT_SIZE) + self.screen = pygame.display.set_mode((self.SCREEN_WIDTH, self.SCREEN_HEIGHT)) + pygame.display.set_caption("Game Over Animation") + + def create_text_surface(self, text): + return self.font.render(text, True, self.TEXT_COLOR) + + def run(self): + running = True + current_text = "" + char_timer = 0 + char_index = 0 + + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + self.screen.fill(self.BACKGROUND_COLOR) + + if char_index < len(self.game_over_text): + current_text += self.game_over_text[char_index] + char_index += 1 + + text_surface = self.create_text_surface(current_text) + + text_rect = text_surface.get_rect() + text_rect.center = (self.SCREEN_WIDTH // 2, self.SCREEN_HEIGHT // 2) + + self.screen.blit(text_surface, text_rect) + pygame.display.flip() + + if char_index < len(self.game_over_text): + time.sleep(1) # Wait 1 second for each character + + pygame.quit() + sys.exit() + + +if __name__ == "__main__": + game = GameOverAnimation() + game.run() From 3110a6cd534d40322fa3a27675991057ec325630 Mon Sep 17 00:00:00 2001 From: Simon Jones Date: Sun, 24 Dec 2023 13:49:50 +0000 Subject: [PATCH 6/6] parking current branch --- classes/Display.py | 31 ++++ .../{Game_controller.py => Game_ontroller.py} | 0 classes/State_machine.py | 150 ++++++++++++++++++ classes/System.py | 122 ++++++++++++++ classes/controllers/Baseline_controller.py | 2 +- classes/controllers/Bomb_controller.py | 46 ++++-- classes/controllers/Input_controller.py | 6 +- classes/controllers/Invader_controller.py | 63 +++++--- classes/controllers/Mothership_controller.py | 2 +- classes/controllers/Player_controller.py | 98 +++++------- .../controllers/Player_missile_controller.py | 31 +++- classes/controllers/Shield_controller.py | 40 +++-- classes/controllers/UI_controller.py | 6 +- .../controllers/UI_game_over_controller.py | 2 +- game.py | 24 ++- lib/Controller.py | 10 +- states/State_game_playing.py | 28 ++++ states/State_game_starts.py | 148 +++++++++++++++++ states/State_intro.py | 52 ++++++ states/State_player_exploding.py | 60 +++++++ 20 files changed, 791 insertions(+), 130 deletions(-) create mode 100644 classes/Display.py rename classes/{Game_controller.py => Game_ontroller.py} (100%) create mode 100644 classes/State_machine.py create mode 100644 classes/System.py create mode 100644 states/State_game_playing.py create mode 100644 states/State_game_starts.py create mode 100644 states/State_intro.py create mode 100644 states/State_player_exploding.py diff --git a/classes/Display.py b/classes/Display.py new file mode 100644 index 0000000..49c1033 --- /dev/null +++ b/classes/Display.py @@ -0,0 +1,31 @@ +import pygame +from classes.config.Game_config import GameConfig + + +class Display: + def __init__(self): + config = GameConfig() + self.top_left = config.get("top_left") + self.original_screen_size = config.get("original_screen_size") + self.larger_screen_size = config.get("larger_screen_size") + + _bg_image = pygame.image.load(config.get_file_path(config.get("bg_image_path"))) + self.scaled_image = pygame.transform.scale(_bg_image, self.larger_screen_size) + self.window_surface = pygame.display.set_mode(self.larger_screen_size) + + def update(self, surface_array): + clean_game_surface = pygame.Surface(self.original_screen_size, pygame.SRCALPHA) + for drawable_surface in surface_array: + # print(drawable_surface) + if isinstance(drawable_surface, pygame.sprite.Group): + # if isinstance(drawable_surface, pygame.Surface): + drawable_surface.draw(clean_game_surface) + + # background image is drawn first + self.window_surface.blit(self.scaled_image, self.top_left) + + # scale the playing surface up to target size on main window + self.window_surface.blit( + pygame.transform.scale(clean_game_surface, self.larger_screen_size), + self.top_left, + ) diff --git a/classes/Game_controller.py b/classes/Game_ontroller.py similarity index 100% rename from classes/Game_controller.py rename to classes/Game_ontroller.py diff --git a/classes/State_machine.py b/classes/State_machine.py new file mode 100644 index 0000000..3f5fcc0 --- /dev/null +++ b/classes/State_machine.py @@ -0,0 +1,150 @@ +from classes.Display import Display + + +# idea: have system class that holds event management and controller management and display +# system has methods to delegate to above system +# then get events and controllers out of state machine +class StateMachine: + def __init__(self, states, starting_state): + self.display = Display() + self.states = states + self.state = states[starting_state] + self.enter_state() + + def get_state_name(self): + return self.state.name + + def change_to(self, new_state): + self.state = self.states[new_state] + self.enter_state() + + def update(self, events): + self.state.update(events) + returned_surfaces = self.state.get_surfaces() + self.display.update(returned_surfaces) + + def enter_state(self): + self.state.enter(self) + + +# extends Node + +# class_name StateMachine + +# signal state_bootup_entered +# signal state_crashed_entered +# signal state_demo_crashed_entered +# signal state_demo_over_entered +# signal state_demo_playing_entered +# signal state_game_over_entered +# signal state_game_over_high_scores_entered +# signal state_intro_entered +# signal state_key_controls_entered +# signal state_high_scores_entered +# signal state_player_starts +# signal state_score_table_entered +# signal state_mission_complete_entered +# signal state_playing_entered +# signal state_settings_entered +# signal state_achievements_entered + + +# const DEBUG = true + +# var state: Object +# var mission_complete:bool = false +# var base_destroyed:bool = false +# var demo_mode: bool = false +# var crashed: bool = false +# var lives_depleted: bool = false + + +# func is_demo_mode() -> bool: +# return demo_mode + + +# func set_demo_mode(value: bool) -> void: +# demo_mode = value + + +# func set_player_crashed() -> void: +# crashed = true + +# func clear_player_crashed() -> void: +# crashed = false + + +# func set_mission_completed() -> void: +# # set the flag in case ship is destroyed during base explosion +# # we want to allow for respawn and immediate exit to congrats screen +# mission_complete = true + + +# func set_base_destroyed(_points) -> void: # signal connects with points +# base_destroyed = true + + +# func set_lives_depleted(): +# lives_depleted = true + + +# func clear_lives_depleted(): +# lives_depleted = false + + +# func get_state_name() -> String: +# return state.name + + +# func _ready() -> void: +# # important - need bootup node first to allow delay +# # otherwise signals do not completely load in game.gd +# state = get_node("State_bootup") +# _enter_state() + + +# func change_to(new_state) -> void: +# state = get_node(new_state) +# _enter_state() + + +# func _input(event) -> void: +# if state.has_method("input"): +# state.input(event) + + +# func _unhandled_key_input(event) -> void: +# if state.has_method("unhandled_key_input"): +# state.unhandled_key_input(event) + + +# func _process(delta) -> void: +# if state.has_method("process"): +# state.process(delta) + + +# func _enter_state() -> void: +# if DEBUG: +# print("Entering state: ", state.name) + +# match state.name: +# "State_bootup": emit_signal("state_bootup_entered") +# "State_intro": emit_signal("state_intro_entered") +# "State_settings": emit_signal("state_settings_entered") +# "State_key_controls": emit_signal("state_key_controls_entered") +# "State_achievements": emit_signal("state_achievements_entered") +# "State_high_scores": emit_signal("state_high_scores_entered") +# "State_score_table": emit_signal("state_score_table_entered") +# "State_player_starts": emit_signal("state_player_starts") +# "State_playing": emit_signal("state_playing_entered") +# "State_demo_playing": emit_signal("state_demo_playing_entered") +# "State_demo_crashed": emit_signal("state_demo_crashed_entered") +# "State_demo_over": emit_signal("state_demo_over_entered") +# "State_crashed": emit_signal("state_crashed_entered") +# "State_game_over": emit_signal("state_game_over_entered") +# "State_game_over_high_scores": emit_signal("state_game_over_high_scores_entered") +# "State_mission_complete": emit_signal("state_mission_complete_entered") + +# # Give the new state a reference to this state machine script +# state.fsm = self +# state.enter() diff --git a/classes/System.py b/classes/System.py new file mode 100644 index 0000000..e9064ea --- /dev/null +++ b/classes/System.py @@ -0,0 +1,122 @@ +import importlib +import inspect +import os +from classes.Display import Display +from lib.Event_manager import EventManager +from lib.Controller import Controller + +## dont forget about callback in controllers +## perhaps pass an notifier callback and register callback as params into controller instancing line 70 +## maybe have a callback class and pass a method or object + + +class System: + _instance = None + + @classmethod + def get_instance(cls): + if cls._instance is None: + cls._instance = System() + return cls._instance + + def __init__(self): + self.display = Display() + self.event_manager = EventManager.get_instance() + self.load_controllers() + + # Class-level dictionary to store callbacks + callbacks = {} + + # Class-level dictionary to store callback names (labels) + callback_names = {} + + @classmethod + def register_callback(cls, key, callback, name=None): + cls.callbacks[key] = callback + + # Store the callback name if provided + if name: + cls.callback_names[key] = name + + @classmethod + def get_callback(cls, key): + return cls.callbacks.get(key, None) + + @classmethod + def callback(cls, key): + callback_function = cls.callbacks.get(key) + if callback_function is not None: + return callback_function() + else: + # Optionally handle the case where the key is not found + # print(f"No callback found for key: {key}") + return None + + @classmethod + def debug_callbacks(cls): + print("Callbacks:") + for key, callback in cls.callbacks.items(): + # Get the callback name from the dictionary, or use the key as a fallback + name = cls.callback_names.get(key, key) + print( + f"{name}: {callback.__name__ if hasattr(callback, '__name__') else callback}" + ) + + def add_listener(self, event_type, listener): + self.event_manager.add_listener(event_type, listener) + + def remove_listener(self, event_type, listener): + self.event_manager.remove_listener(event_type, listener) + + def get_controller(self, target_controller): + _controller = next( + ( + controller + for controller in self.controllers + if controller.__class__.__name__.replace("Controller", "") + == target_controller + ), + None, + ) + return _controller + + def load_controllers(self): + # Initialize an empty list to store the imported controllers + self.controllers = [] + + # Construct the full directory path for controllers + controllers_directory = os.path.join("classes", "controllers") + + # Loop through files in the controllers directory + for filename in os.listdir(controllers_directory): + if filename.endswith("_controller.py"): + # Extract the module name without the extension + module_name = filename[:-3] + + # Construct the full module path + module_path = f"classes.controllers.{module_name}" + + # Import the module dynamically + module = importlib.import_module(module_path) + + # Check if the module defines a controller class and add it to the list + for name, obj in inspect.getmembers(module): + if ( + inspect.isclass(obj) + and issubclass(obj, Controller) + and obj != Controller + ): + # Create an instance of the controller + controller_instance = obj() ### inject notifier here + if not hasattr(controller_instance, "rendering_order"): + controller_instance.rendering_order = 0 + + self.controllers.append(controller_instance) + + self.controllers.sort(key=lambda controller: controller.rendering_order) + + for controller_instance in self.controllers: + if hasattr(controller_instance, "game_ready") and callable( + controller_instance.game_ready + ): + controller_instance.game_ready() diff --git a/classes/controllers/Baseline_controller.py b/classes/controllers/Baseline_controller.py index 2bd3584..ea14e1b 100644 --- a/classes/controllers/Baseline_controller.py +++ b/classes/controllers/Baseline_controller.py @@ -20,7 +20,7 @@ def draw(self, surface): # draw the baselineSprite onto the specified surface surface.blit(self.baselineSprite.image, self.baselineSprite.rect.topleft) - def update(self, events, dt): + def update(self, events, state): bomb_callback = self.get_callback("get_bombs") bomb_sprites = bomb_callback() if len(bomb_sprites) > 0: diff --git a/classes/controllers/Bomb_controller.py b/classes/controllers/Bomb_controller.py index 2a9aedb..9c37b64 100644 --- a/classes/controllers/Bomb_controller.py +++ b/classes/controllers/Bomb_controller.py @@ -10,6 +10,8 @@ class BombController(Controller): def __init__(self): super().__init__() + self.state = {} + self.counter = 0 self.enabled = False self.max_bombs = 2 @@ -22,14 +24,21 @@ def __init__(self): self.event_manager.add_listener("play_delay_complete", self.on_player_ready) self.event_manager.add_listener("player_explodes", self.on_player_explodes) - self.register_callback("get_bombs", lambda: self.bomb_container.get_bombs()) + #self.register_callback("get_bombs", lambda: self.bomb_container.get_bombs()) + + self.get_invaders_callback = None + self.get_player_callback = None + self.get_invaders_clearpath_callback = None def game_ready(self): - self.get_invaders_callback = self.get_callback("get_invaders") - self.get_player_callback = self.get_callback("get_player") - self.get_invaders_clearpath_callback = self.get_callback( - "get_invaders_with_clear_path" - ) + pass + #self.get_invaders_callback = self.get_callback("get_invaders") + #self.get_player_callback = self.get_callback("get_player") + #self.get_invaders_clearpath_callback = self.get_callback( + # "get_invaders_with_clear_path" + #) + def get_bombs(self): + return self.bomb_container.get_bombs() def on_player_explodes(self, data): self.enabled = False @@ -37,14 +46,24 @@ def on_player_explodes(self, data): def on_player_ready(self, data): self.enabled = True - def update(self, events, dt): + def get_surface(self): + return self.bomb_container + + def update(self, events, state): + self.state = state invaders = self.get_invaders_callback() - if len(invaders) > 0 and self.enabled == True: - # create a new bomb - if len(self.bomb_container.get_bombs()) < self.max_bombs: - bomb = self.create_bomb() - if isinstance(bomb, Bomb): - self.bomb_container.add(bomb) + + # if 'fire_button_pressed' in self.state: + # if self.state['fire_button_pressed']: + + if len(invaders) > 0: + if 'bombs_enabled' in self.state: + if self.state['bombs_enabled'] == True: + # create a new bomb + if len(self.bomb_container.get_bombs()) < self.max_bombs: + bomb = self.create_bomb() + if isinstance(bomb, Bomb): + self.bomb_container.add(bomb) # Update all existing bomb sprites in this container self.counter += 1 @@ -52,7 +71,6 @@ def update(self, events, dt): self.counter = 0 for sprite in self.bomb_container: sprite.update() - return self.bomb_container def create_bomb(self): def get_next_bomb_type(): diff --git a/classes/controllers/Input_controller.py b/classes/controllers/Input_controller.py index aaa1158..c3959e8 100644 --- a/classes/controllers/Input_controller.py +++ b/classes/controllers/Input_controller.py @@ -10,10 +10,14 @@ class InputController(Controller): def __init__(self): super().__init__() - def update(self, events, dt): + def update(self, events, state): + # self.event_manager.notify("escape_button_pressed") + # def update(self, events, dt): for event in events: + # print("event", event) if event.type == KEYDOWN: if event.key == K_ESCAPE: + # print("escape press in input controller update") self.event_manager.notify("escape_button_pressed") if event.key == K_k: # 'K' key pressed diff --git a/classes/controllers/Invader_controller.py b/classes/controllers/Invader_controller.py index 46e0fb4..a4f2562 100644 --- a/classes/controllers/Invader_controller.py +++ b/classes/controllers/Invader_controller.py @@ -7,35 +7,48 @@ class InvaderController(Controller): def __init__(self): super().__init__() + self.state = {} + invader_factory = InvaderFactory() self.invader_generator = invader_factory.create_invader_swarm() self.invader_container = InvaderContainer() - self.is_moving = False self.swarm_complete = False self.countdown = 0 - self.event_manager.add_listener("invader_hit", self.on_invader_hit) - self.event_manager.add_listener("player_explodes", self.on_player_explodes) + + + # can we do this outside of the controller like in the state + # suppose we follow the react method of having access to state setting, + #self.event_manager.add_listener("invader_hit", self.on_invader_hit) + #self.event_manager.add_listener("player_explodes", self.on_player_explodes) self.event_manager.add_listener("play_delay_complete", self.on_player_ready) - self.register_callback( - "get_invaders", lambda: self.invader_container.get_invaders() - ) + # self.register_callback( + # "get_invaders", lambda: self.invader_container.get_invaders() + # ) + + # self.register_callback( + # "get_invaders_with_clear_path", + # lambda: self.invader_container.get_invaders_with_clear_path(), + # ) + + # self.register_callback( + # "get_invader_count", lambda: len(self.invader_container.get_invaders()) + # ) - self.register_callback( - "get_invaders_with_clear_path", - lambda: self.invader_container.get_invaders_with_clear_path(), - ) + # self.register_callback( + # "get_lowest_invader_y", + # lambda: self.invader_container.get_invaders()[0].rect.y, + # ) - self.register_callback( - "get_invader_count", lambda: len(self.invader_container.get_invaders()) - ) + # maybe this could be in base controller + # injects functions that can get state and change state + def set_up_state(self, fn_state_change, fn_get_state): + pass - self.register_callback( - "get_lowest_invader_y", - lambda: self.invader_container.get_invaders()[0].rect.y, - ) + # should this be in the state rather than controller + # this isn't core functionality and appears to be controlling state of vars def game_restart(self): invader_factory = InvaderFactory() self.invader_generator = invader_factory.create_invader_swarm() @@ -44,6 +57,12 @@ def game_restart(self): self.swarm_complete = False self.countdown = 0 + def get_invaders(self): + return self.invader_container.get_invaders() + # suppose we get vars like coundown from the state object + # and vars like is_moving + # function get state set state + # like in react def on_invader_hit(self, invader): self.is_moving = False # pause invaders 1/4 second (60/15) @@ -74,7 +93,13 @@ def release_non_active(self): self.is_moving = True self.event_manager.notify("invader_removed") - def update(self, events, dt): + def get_surface(self): + return self.invader_container + + # have another function that receives state? + def update(self, events, state): + self.state = state + self.is_moving = state['invaders_moving'] if not self.swarm_complete: self.generate_next_invader() else: @@ -85,5 +110,3 @@ def update(self, events, dt): if self.is_moving: self.invader_container.update() - - return self.invader_container diff --git a/classes/controllers/Mothership_controller.py b/classes/controllers/Mothership_controller.py index 066fb7a..ecd01fe 100644 --- a/classes/controllers/Mothership_controller.py +++ b/classes/controllers/Mothership_controller.py @@ -43,7 +43,7 @@ def mothership_is_exploding(self): if self.mothership_group.sprites()[0].active == False: return True - def update(self, events, dt): + def update(self, events, state): if not self.spawned: self.update_spawn_logic() else: diff --git a/classes/controllers/Player_controller.py b/classes/controllers/Player_controller.py index bc5918f..beea1cd 100644 --- a/classes/controllers/Player_controller.py +++ b/classes/controllers/Player_controller.py @@ -4,44 +4,23 @@ player_speed = 1 -# PLAY_DELAY_COMPLETE_EVENT = "play_delay_complete" -# LEFT_BUTTON_PRESSED_EVENT = "left_button_pressed" -# LEFT_BUTTON_RELEASED_EVENT = "left_button_released" -# RIGHT_BUTTON_PRESSED_EVENT = "right_button_pressed" -# RIGHT_BUTTON_RELEASED_EVENT = "right_button_released" -# PLAYER_EXPLOSION_COMPLETE_EVENT = "player_explosion_complete" - - class PlayerController(Controller): def __init__(self): super().__init__() - self.can_launch_missile = True - self.enabled = False + + self.state = {} + self.spawned = False self.is_exploding = False + self.exploding_animation_complete = False # player group has one player sprite self.player_group_sprites = pygame.sprite.Group() - self.left_key_pressed = False - self.right_key_pressed = False - - self.register_callback("get_player", self.get_player) - self.register_callback("spawn_player", self.spawn_player) - - self.event_manager.add_listener( - "play_delay_complete", self.on_play_delay_complete - ) - self.event_manager.add_listener("left_button_pressed", self.on_move_left) - self.event_manager.add_listener("left_button_released", self.on_move_left_exit) - self.event_manager.add_listener("right_button_pressed", self.on_move_right) - self.event_manager.add_listener( - "right_button_released", self.on_move_right_exit - ) + def spawn_player(self): self.spawned = True - self.enabled = True params = { "player_x_position": 10, "player_y_position": 219, @@ -49,42 +28,40 @@ def spawn_player(self): player = Player(params) self.player_group_sprites.add(player) - def on_play_delay_complete(self, data): - self.spawn_player() - - def on_move_left_exit(self, data): - self.left_key_pressed = False - def on_move_left(self, data): - self.left_key_pressed = True + def explode_player(self): + print("exploding player") + self.is_exploding = True + self.get_player().explode() - def on_move_right_exit(self, data): - self.right_key_pressed = False - def on_move_right(self, data): - self.right_key_pressed = True + def get_surface(self): + return self.player_group_sprites - def update(self, events, dt): + def update(self, events, state): + self.state = state self.check_bomb_collisions() if self.get_player(): - if self.enabled: - return self.update_player() - elif self.is_exploding: - return self.get_player().update() - + if 'player_enabled' in self.state: + if self.state['player_enabled'] == True: + self.update_player() + elif self.is_exploding: + self.get_player().update() else: if self.is_exploding: print("end of explosion") self.event_manager.notify("player_explosion_complete") + self.exploding_animation_complete = True self.is_exploding = False + def update_player(self): player = self.get_player() - if self.left_key_pressed: + if self.state['is_moving_left']: player.rect.x -= player_speed - elif self.right_key_pressed: + elif self.state['is_moving_right']: player.rect.x += player_speed - return player.update() + player.update() def clamp(value, min_value, max_value): return max(min(value, max_value), min_value) @@ -94,17 +71,18 @@ def get_player(self): return self.player_group_sprites.sprites()[0] def check_bomb_collisions(self): - if self.enabled: - bomb_sprites = self.callback("get_bombs") - - if bomb_sprites is not None: - for bomb_sprite in bomb_sprites: - if bomb_sprite.active and pygame.sprite.collide_mask( - self.get_player(), bomb_sprite - ): - bomb_sprite.explode() - print("hit") - self.is_exploding = True - self.enabled = False - self.get_player().explode() - self.event_manager.notify("player_explodes") + if 'player_enabled' in self.state: + if self.state['player_enabled'] == True: + bomb_sprites = self.callback("get_bombs") + + if bomb_sprites is not None: + for bomb_sprite in bomb_sprites: + if bomb_sprite.active and pygame.sprite.collide_mask( + self.get_player(), bomb_sprite + ): + bomb_sprite.explode() + # print("here..") + # self.is_exploding = True + # self.player_enabled = False + # self.get_player().explode() + self.event_manager.notify("player_explodes") diff --git a/classes/controllers/Player_missile_controller.py b/classes/controllers/Player_missile_controller.py index aef8cd7..797e1d5 100644 --- a/classes/controllers/Player_missile_controller.py +++ b/classes/controllers/Player_missile_controller.py @@ -6,6 +6,8 @@ class PlayerMissileController(Controller): def __init__(self): super().__init__() + self.state = {} + self.ready_flag = False self.rendering_order = 1 # there will only ever be one sprite in this group @@ -15,7 +17,10 @@ def __init__(self): self.event_manager.add_listener("invader_removed", self.on_missile_ready) self.event_manager.add_listener("play_delay_complete", self.on_missile_ready) - self.event_manager.add_listener("fire_button_pressed", self.on_fire_pressed) + + self.get_player_callback = None + self.get_invaders_callback = None + def game_ready(self): return @@ -23,13 +28,17 @@ def game_ready(self): def on_missile_ready(self, data): self.ready_flag = True - def on_fire_pressed(self, data): + def on_fire_pressed(self): + + + + if ( not self.missile_group and self.ready_flag and not self.callback("mothership_is_exploding") ): - player = self.callback("get_player") + player = self.get_player_callback() #self.callback("get_player") params = { "player_x_position": player.rect.x, "player_y_position": player.rect.y, @@ -38,22 +47,32 @@ def on_fire_pressed(self, data): def check_invader_collisions(self): if self.missile_group: - invaders = self.callback("get_invaders") + invaders = self.get_invaders_callback() #self.callback("get_invaders") missile = self.get_player_missile() for invader_sprite in invaders: collision_area = pygame.sprite.collide_mask(invader_sprite, missile) if collision_area is not None: + print("missile hit") self.event_manager.notify("invader_hit", invader_sprite) self.ready_flag = False return True - def update(self, events, dt): + def update(self, events, state): + self.state = state + if 'fire_button_pressed' in self.state: + if self.state['fire_button_pressed']: + self.on_fire_pressed() + if self.missile_group: if not self.check_invader_collisions(): - return self.get_player_missile().update() + self.get_player_missile().update() else: self.missile_group.remove(self.get_player_missile()) + def get_surface(self): + return self.missile_group + + # shields need access to player missile def get_player_missile(self): if self.missile_group: diff --git a/classes/controllers/Shield_controller.py b/classes/controllers/Shield_controller.py index 70b9211..b324f92 100644 --- a/classes/controllers/Shield_controller.py +++ b/classes/controllers/Shield_controller.py @@ -10,28 +10,39 @@ def __init__(self): self.rendering_order = -1 self.shield_container = ShieldFactory().create_shields() + self.get_invaders_callback = None + self.get_bombs_callback = None + self.get_player_missile_callback = None + # place any code here that should run when controllers have loaded def game_ready(self): return - def update(self, events, dt): - self.check_bomb_collisions() - self.check_invader_collision() - self.check_missile_collision() + def get_surface(self): return self.shield_container + def update(self, events, state): + pass + # get_invaders_callback = self.get_callback("get_invaders") + # print(get_invaders_callback) + #self.check_bomb_collisions() + #self.check_invader_collision() + #self.check_missile_collision() + # return self.shield_container + def check_missile_collision(self): - missile = self.callback("get_player_missile") + missile = self.get_player_missile_callback + print(self.get_player_missile_callback) - if missile is not None and missile.active: - for shield_sprite in self.shield_container: - collision_area = pygame.sprite.collide_mask(shield_sprite, missile) - if collision_area is not None: - shield_sprite.missile_collision(collision_area) - missile.explode((-4, 2)) + # if missile is not None and missile.active: + # for shield_sprite in self.shield_container: + # collision_area = pygame.sprite.collide_mask(shield_sprite, missile) + # if collision_area is not None: + # shield_sprite.missile_collision(collision_area) + # missile.explode((-4, 2)) def check_invader_collision(self): - invaders = self.callback("get_invaders") + invaders = self.get_invaders_callback() if invaders: collisions = pygame.sprite.groupcollide( self.shield_container, invaders, False, False @@ -41,9 +52,8 @@ def check_invader_collision(self): shield_sprite.invader_damage(invader) def check_bomb_collisions(self): - bomb_sprites = self.callback("get_bombs") - - if bomb_sprites is not None: + bomb_sprites = self.get_bombs_callback() + if bomb_sprites: for shield_sprite in self.shield_container: for bomb_sprite in bomb_sprites: if bomb_sprite.active and pygame.sprite.collide_mask( diff --git a/classes/controllers/UI_controller.py b/classes/controllers/UI_controller.py index b6f23f3..a1e12db 100644 --- a/classes/controllers/UI_controller.py +++ b/classes/controllers/UI_controller.py @@ -61,15 +61,15 @@ def draw_lives(self): def draw(self, surface): surface.blit(self.canvas, (0, 0)) # blit the canvas onto the game surface - def update(self, events, dt): + def update(self, events, state): # clear the canvas between each draw self.canvas.fill((0, 0, 0, 0)) # get the score value using the callback to the scoreboard controller score = self.callback("get_score") - lives_canvas = self.draw_lives() - self.canvas.blit(lives_canvas, (1, 242)) + # lives_canvas = self.draw_lives() + # self.canvas.blit(lives_canvas, (1, 242)) # position SCORE text at position in config self.canvas.blit( diff --git a/classes/controllers/UI_game_over_controller.py b/classes/controllers/UI_game_over_controller.py index a8bf416..0d1ea66 100644 --- a/classes/controllers/UI_game_over_controller.py +++ b/classes/controllers/UI_game_over_controller.py @@ -18,7 +18,7 @@ def on_game_over(self, data): print("on game over") self.game_over_text_running = True - def update(self, events, dt): + def update(self, events, state): self.canvas.fill((0, 0, 0, 0)) if self.game_over_text_running: diff --git a/game.py b/game.py index 15621b4..f940958 100644 --- a/game.py +++ b/game.py @@ -1,17 +1,28 @@ import pygame, time from pygame.locals import * -from classes.Game_controller import GameController + +from states.State_intro import StateIntro +from states.State_game_starts import StateGameStarts +from states.State_game_playing import StateGamePlaying +from states.State_player_exploding import StatePlayerExploding +from classes.State_machine import StateMachine pygame.init() -game_controller = GameController() -game_controller.load_controllers() +states = { + "GAME_INTRO": StateIntro(), + "GAME_START": StateGameStarts(), + "GAME_PLAYING": StateGamePlaying(), + "PLAYER_EXPLODING": StatePlayerExploding(), +} + +# system = System() +state_machine = StateMachine(states, "GAME_INTRO") + running = True max_fps = 60 clock = pygame.time.Clock() while running: - dt = 0 - events = [] for event in pygame.event.get(): if event.type == QUIT: @@ -19,7 +30,8 @@ else: events.append(event) - game_controller.update(events, dt) + state_machine.update(events) + pygame.display.flip() clock.tick(max_fps) diff --git a/lib/Controller.py b/lib/Controller.py index b57489d..caa9119 100644 --- a/lib/Controller.py +++ b/lib/Controller.py @@ -21,11 +21,17 @@ def register_callback(cls, key, callback, name=None): @classmethod def get_callback(cls, key): - return cls.callbacks.get(key) + return cls.callbacks.get(key, None) @classmethod def callback(cls, key): - return cls.callbacks.get(key)() + callback_function = cls.callbacks.get(key) + if callback_function is not None: + return callback_function() + else: + # Optionally handle the case where the key is not found + # print(f"No callback found for key: {key}") + return None @classmethod def debug_callbacks(cls): diff --git a/states/State_game_playing.py b/states/State_game_playing.py new file mode 100644 index 0000000..589ed02 --- /dev/null +++ b/states/State_game_playing.py @@ -0,0 +1,28 @@ +class StateGamePlaying: + def __init__(self): + print("initialised state playing") + # self.state_machine = state_machine + ## maybe load in specific controllers here? + + ## how to get controllers to indicate when the state has changed + ## use callbacks or events? + ## pass the statemachine/state to each controller? + ## have the game controller receive the statemachine? + + ## give the invader_controller a hook into this code + ## have invader_controller call a function in this state when an event happens + + # load controllers needed + # add events betwen the controller back to the state functions + # invader_controller.event_manager.add_listener("invader_hit", self.on_invader_hit) + + def enter(self): + # code that should run when entering this state + print("entered state game playing") + + def update(self): + # code that should run every frame + print("in update on game playing") + + def exit(self, next_state): + self.state_machine.change_to(next_state) diff --git a/states/State_game_starts.py b/states/State_game_starts.py new file mode 100644 index 0000000..f54e37e --- /dev/null +++ b/states/State_game_starts.py @@ -0,0 +1,148 @@ +from classes.System import System + +# hold all game state here for all controllers to use +# allow controllers to access state related to other controllers? +# create a basic state class which has common functions +class StateGameStarts: + def __init__(self): + self.system = System.get_instance() + + # we'll pass the state array to all controllers so they can share the state + # and we'll allow controllers to update the state back up + self.state = { + 'invaders_moving': True, + 'is_moving_left': False, + 'is_moving_right': False, + 'fire_button_pressed': False, + 'player_enabled': True, + 'bombs_enabled': True, + } + + # self.state = { + # 'invaders_moving': False, + # 'is_moving_left': False, + # 'is_moving_right': False, + # + # 'fire_button_pressed': { + # 'default': False,#needed if reseting use default + # 'one_shot': True, + # 'value': False + # } + # } + + print("initialised state game") + + def reset_state(self, index): + pass + # this would return the state identified by index back to its reset value + + def update_state(returned_state): + self.state = returned_state + + # update to be able to specify 'one shot' = True + # could reset on subsequent passes + ### maybe even store previous value #### + def set_state(self, index, value): + self.state[index] = value + + def get_state_value(self, index): + if index in self.state: + return self.state[index] + + + def enter(self, state_machine): + self.state_machine = state_machine + + self.invader_controller = self.system.get_controller("Invader") + self.input_controller = self.system.get_controller("Input") + self.shield_controller = self.system.get_controller("Shield") + self.bomb_controller = self.system.get_controller("Bomb") + self.player_controller = self.system.get_controller("Player") + self.player_missile_controller = self.system.get_controller("PlayerMissile") + + + self.bomb_controller.enabled = True + self.player_controller.spawn_player() + self.player_missile_controller.ready_flag = True + + #self.system.add_listener("player_explodes", self.on_player_hit) + self.system.add_listener("invader_hit", self.invader_controller.on_invader_hit) + + + #player events + self.system.add_listener("left_button_pressed", self.on_move_left) + self.system.add_listener("left_button_released", self.on_move_left_exit) + self.system.add_listener("right_button_pressed", self.on_move_right) + self.system.add_listener("right_button_released", self.on_move_right_exit) + self.system.add_listener("fire_button_pressed", self.on_fire_pressed) + + #self.system.register_callback("get_player", self.player_controller.get_player) + + self.player_missile_controller.get_player_callback = self.player_controller.get_player + self.player_missile_controller.get_invaders_callback = self.invader_controller.get_invaders + + self.bomb_controller.get_invaders_callback = self.invader_controller.get_invaders + self.bomb_controller.get_player_callback = self.player_controller.get_player + self.bomb_controller.get_invaders_clearpath_callback = self.invader_controller.invader_container.get_invaders_with_clear_path + + self.shield_controller.get_invaders_callback = self.invader_controller.get_invaders + self.shield_controller.get_bombs_callback = self.bomb_controller.get_bombs + + self.shield_controller.get_player_missile_callback = self.player_missile_controller.get_player_missile + + print(self.shield_controller.get_player_missile_callback) + + + #self.system.register_callback("get_invaders", self.invader_controller.get_invaders) + #self.system.register_callback("get_invaders_with_clear_path", lambda: self.invader_controller.invader_container.get_invaders_with_clear_path()) + #self.system.register_callback("get_lowest_invader_y", lambda: self.invader_controller.invader_container.get_invaders()[0].rect.y) + #self.system.register_callback("get_invader_count", lambda: len(self.invader_controller.invader_container.get_invaders())) + + + + # have a function that sets state on a index of the dict and specify whether it clears or changes on subsequent updates + + def on_fire_pressed(self, data): + self.set_state('fire_button_pressed', True) + + def on_move_left(self, data): + self.set_state('is_moving_left', True) + + def on_move_left_exit(self, data): + self.set_state('is_moving_left', False) + + def on_move_right(self, data): + self.set_state('is_moving_right', True) + + def on_move_right_exit(self, data): + self.set_state('is_moving_right', False) + + def on_fire_button_pressed(self, data): + print("fire button caught in state game starts") + self.exit("GAME_INTRO") + + def on_player_hit(self, data): + print("player was hit") + #self.exit("PLAYER_EXPLODING") + + def update(self, events): + self.input_controller.update(events, self.state) + self.invader_controller.update(events, self.state) + self.shield_controller.update(events, self.state) + self.bomb_controller.update(events, self.state) + self.player_controller.update(events, self.state) + self.player_missile_controller.update(events, self.state) + self.set_state('fire_button_pressed', False) + + def get_surfaces(self): + return [ + self.player_missile_controller.get_surface(), + self.invader_controller.get_surface(), + self.shield_controller.get_surface(), + self.bomb_controller.get_surface(), + self.player_controller.get_surface(), + + ] + + def exit(self, next_state): + self.state_machine.change_to(next_state) diff --git a/states/State_intro.py b/states/State_intro.py new file mode 100644 index 0000000..d1c5958 --- /dev/null +++ b/states/State_intro.py @@ -0,0 +1,52 @@ +from classes.System import System + + +class StateIntro: + def __init__(self): + self.system = System.get_instance() + # at this point there is no concept of state machine + print("initialised state intro") + self.state = {} + + # inject functions rather than whole statemachine + # maybe inject + def enter(self, state_machine): + self.state['invaders_moving'] = True + self.state_machine = state_machine + self.invader_controller = self.system.get_controller("Invader") + self.input_controller = self.system.get_controller("Input") + self.shield_controller = self.system.get_controller("Shield") + + # code that should run when entering this state + self.system.add_listener("escape_button_pressed", self.on_escape_button_pressed) + # self.system.add_listener("invader_hit", on_invader_hit) + # def on_invader_hit() self.invader_controller.... + + self.system.register_callback("get_invaders", self.invader_controller.get_invaders) + + self.system.debug_callbacks() + get_invaders_callback = self.system.get_callback("get_invaders") + #print(get_invaders_callback) + + print("entered state intro state") + + def update(self, events): + self.input_controller.update(events, self.state) + self.invader_controller.update(events, self.state), + self.shield_controller.update(events, self.state), + + def get_surfaces(self): + return [ + self.invader_controller.get_surface(), + self.shield_controller.get_surface(), + ] + + def on_escape_button_pressed(self, data): + print("escape caught in state intro") + self.exit("GAME_START") + + def exit(self, next_state): + self.system.remove_listener( + "escape_button_pressed", self.on_escape_button_pressed + ) + self.state_machine.change_to(next_state) diff --git a/states/State_player_exploding.py b/states/State_player_exploding.py new file mode 100644 index 0000000..db6fd03 --- /dev/null +++ b/states/State_player_exploding.py @@ -0,0 +1,60 @@ +from classes.System import System + + +class StatePlayerExploding: + def __init__(self): + self.system = System.get_instance() + + self.state = { + 'invaders_moving': False, + } + + print("initialised state StatePlayerExploding") + # self.event_manager.add_listener( + # "player_explosion_complete", self.on_player_explosion_complete + # ) + + def set_state(self, index, value): + self.state[index] = value + + def get_state_value(self, index): + if index in self.state: + return self.state[index] + + def enter(self, state_machine): + print("entered player explode state") + self.state_machine = state_machine + + self.invader_controller = self.system.get_controller("Invader") + self.shield_controller = self.system.get_controller("Shield") + self.bomb_controller = self.system.get_controller("Bomb") + self.player_controller = self.system.get_controller("Player") + self.player_missile_controller = self.system.get_controller("PlayerMissile") + + self.bomb_controller.enabled = False + self.player_controller.explode_player() + #self.player_missile_controller.get_player_callback = self.player_controller.get_player + + def on_player_explosion_complete(self, data): + print("in state explode - animation complete") + + def update(self, events): + self.invader_controller.update(events, self.state) + self.shield_controller.update(events, self.state) + self.bomb_controller.update(events, self.state) + self.player_controller.update(events, self.state) + self.player_missile_controller.update(events, self.state) + + # use array and look for method on controller get_surface + def get_surfaces(self): + return [ + self.player_missile_controller.get_surface(), + self.invader_controller.get_surface(), + self.shield_controller.get_surface(), + self.bomb_controller.get_surface(), + self.player_controller.get_surface(), + ] + + + def exit(self, next_state): + self.state_machine.change_to(next_state)