From 5de37d121a31da50eecf469b851d230edfcc66dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Fri, 28 Mar 2025 10:18:26 +0100 Subject: [PATCH] Refactor(Game) - Introduce GameResources class to centralize game constants and resources; update related classes to use GameResources for improved maintainability --- main.py | 45 ++++++++++++--------------- src/Camera.py | 8 ++--- src/Entity/Checkpoint.py | 1 - src/Entity/Enemy.py | 4 +-- src/Entity/Player.py | 53 ++++++++++++++++++++++---------- src/Map/parser.py | 20 ++++++------ src/Menu/Button.py | 3 +- src/Menu/Leaderboard.py | 24 ++++++++++----- src/Menu/Menu.py | 20 ++++++------ src/constant.py | 49 +++++++++++++++++------------ src/game.py | 66 ++++++++++++++++++---------------------- 11 files changed, 159 insertions(+), 134 deletions(-) diff --git a/main.py b/main.py index 3aa8084..50ba76f 100644 --- a/main.py +++ b/main.py @@ -4,17 +4,7 @@ from pygame.locals import * from src.Entity.Enemy import Enemy from src.game import initialize_game, reset_game, reset_game_with_checkpoint -from src.constant import ( - displaysurface, - FramePerSec, - font, - FPS, - WIDTH, - HEIGHT, - ORIGINAL_WIDTH, - ORIGINAL_HEIGHT, - fullscreen, -) +from src.constant import GameResources from src.Menu.Menu import Menu from src.Menu.Leaderboard import Leaderboard from src.Camera import Camera @@ -22,11 +12,20 @@ from src.Database.CheckpointDB import CheckpointDB def main(): - # Declare globals that we'll modify - global displaysurface, fullscreen, ORIGINAL_WIDTH, ORIGINAL_HEIGHT + # Initialize Pygame and game resources + game_resources = GameResources() + displaysurface = game_resources.displaysurface + FramePerSec = game_resources.FramePerSec + font = game_resources.font + FPS = game_resources.FPS + WIDTH = game_resources.WIDTH + HEIGHT = game_resources.HEIGHT + ORIGINAL_WIDTH = game_resources.ORIGINAL_WIDTH + ORIGINAL_HEIGHT = game_resources.ORIGINAL_HEIGHT + fullscreen = game_resources.fullscreen # Add camera initialization - camera = Camera(WIDTH, HEIGHT) + camera = Camera(WIDTH, HEIGHT, game_resources) # Game states MENU = 0 @@ -36,12 +35,12 @@ def main(): # Initialize game state and objects current_state = MENU - menu = Menu() - leaderboard = Leaderboard() + menu = Menu(game_resources) + leaderboard = Leaderboard(WIDTH, HEIGHT, font) # Initialize game components P1, PT1, platforms, all_sprites, background, checkpoints = initialize_game( - "map_test.json" + game_resources, "map_test.json" ) projectiles = pygame.sprite.Group() @@ -88,14 +87,12 @@ def main(): if checkpoint_pos: # Respawn player at checkpoint P1, platforms, all_sprites, background, checkpoints = ( - reset_game_with_checkpoint("map_test.json") + reset_game_with_checkpoint("map_test.json", game_resources) ) projectiles.empty() - print("Joueur réanimé au checkpoint") else: # No checkpoint found, return to menu current_state = MENU - print("Game over - retour au menu") if event.dict.get("action") == "create_projectile": projectile = event.dict.get("projectile") projectiles.add(projectile) @@ -104,7 +101,9 @@ def main(): if current_state == MENU: action = menu.handle_event(event) if action == "play": - P1, platforms, all_sprites, background, checkpoints = reset_game() + P1, platforms, all_sprites, background, checkpoints = reset_game( + game_resources + ) current_state = PLAYING elif action == "infinite": current_state = INFINITE @@ -185,10 +184,6 @@ def main(): camera_adjusted_rect.y += camera.camera.y displaysurface.blit(projectile.surf, camera_adjusted_rect) - print( - f"Projectile: pos={projectile.pos}, rect={projectile.rect}, camera={camera.camera}" - ) - checkpoints_hit = pygame.sprite.spritecollide(P1, checkpoints, False) for checkpoint in checkpoints_hit: checkpoint.activate() diff --git a/src/Camera.py b/src/Camera.py index 1351699..d7de76a 100644 --- a/src/Camera.py +++ b/src/Camera.py @@ -1,18 +1,18 @@ # src/Camera.py import pygame -from src.constant import WIDTH, HEIGHT class Camera: - def __init__(self, width, height): + def __init__(self, width, height, game_resources): self.camera = pygame.Rect(0, 0, width, height) self.width = width self.height = height + self.game_resources = game_resources def update(self, target): # Center the target in the camera view - x = -target.rect.centerx + WIDTH // 2 - y = -target.rect.centery + HEIGHT // 2 + x = -target.rect.centerx + self.game_resources.WIDTH // 2 + y = -target.rect.centery + self.game_resources.HEIGHT // 2 # Update camera position self.camera = pygame.Rect(x, y, self.width, self.height) diff --git a/src/Entity/Checkpoint.py b/src/Entity/Checkpoint.py index 2e46dad..c60ccfa 100644 --- a/src/Entity/Checkpoint.py +++ b/src/Entity/Checkpoint.py @@ -43,6 +43,5 @@ class Checkpoint(Entity): # Save checkpoint to database self.db.save_checkpoint(self.map_name, self.pos.x, self.pos.y) - print("checkpoint") return True return False diff --git a/src/Entity/Enemy.py b/src/Entity/Enemy.py index 8c74aa8..8f3f6d4 100644 --- a/src/Entity/Enemy.py +++ b/src/Entity/Enemy.py @@ -1,6 +1,5 @@ import pygame from src.Entity.Entity import Entity -from src.constant import FPS from pygame.math import Vector2 as vec from src.Entity.Projectile import Projectile @@ -98,7 +97,7 @@ class Enemy(Entity): else: self.detected_player = False - def stationary_attack(self, player): + def stationary_attack(self, player, FPS=60): """Remote attack for turret-like enemies""" distance_to_player = vec( player.pos.x - self.pos.x, player.pos.y - self.pos.y @@ -140,7 +139,6 @@ class Enemy(Entity): self.health -= amount if self.health <= 0: self.kill() - print("Enemy killed!") return True return False diff --git a/src/Entity/Player.py b/src/Entity/Player.py index 772e133..8a49342 100644 --- a/src/Entity/Player.py +++ b/src/Entity/Player.py @@ -1,14 +1,16 @@ from src.Entity.Entity import Entity -from src.constant import WIDTH, vec, ACC, FRIC, platforms, FPS, life_icon_width from pygame import * import pygame import os class Player(Entity): - def __init__(self, width=100, height=100, x=10, y=385): + def __init__(self, game_resources, width=100, height=100, x=10, y=385): super().__init__(pos=(x, y), size=(width, height), color=(128, 255, 40)) + # Game ressources + self.game_resources = game_resources + # Animation variables self.animation_frames = [] self.jump_frames = [] @@ -106,11 +108,20 @@ class Player(Entity): "assets/player/Sanic Head.png" ).convert_alpha() self.life_icon = pygame.transform.scale( - self.life_icon, (life_icon_width, life_icon_width) + self.life_icon, + ( + self.game_resources.life_icon_width, + self.game_resources.life_icon_width, + ), ) else: # Backup: use a red square - self.life_icon = pygame.Surface((life_icon_width, life_icon_width)) + self.life_icon = pygame.Surface( + ( + self.game_resources.life_icon_width, + self.game_resources.life_icon_width, + ) + ) self.life_icon.fill((255, 0, 0)) except Exception as e: @@ -155,7 +166,7 @@ class Player(Entity): if self.dashing and current_time - self.dash_start_time >= self.dash_duration: self.dashing = False - self.acc = vec(0, 1) # Gravity + self.acc = self.game_resources.vec(0, 1) # Reset flags self.moving = False @@ -164,15 +175,15 @@ class Player(Entity): if pressed_keys[K_q]: # Check if X is > 0 to prevent player from going off screen if self.pos.x > 0: - self.acc.x = -ACC + self.acc.x = -self.game_resources.ACC self.moving = True if pressed_keys[K_a]: - self.dash(-ACC) + self.dash(-self.game_resources.ACC) if pressed_keys[K_d]: - self.acc.x = ACC + self.acc.x = self.game_resources.ACC self.moving = True if pressed_keys[K_a]: - self.dash(ACC) + self.dash(self.game_resources.ACC) # Also consider the player moving if they have significant horizontal velocity if abs(self.vel.x) > 0.5: @@ -184,8 +195,8 @@ class Player(Entity): self.jumping = True # Apply friction - self.acc.y += self.vel.y * FRIC - self.acc.x += self.vel.x * FRIC + self.acc.y += self.vel.y * self.game_resources.FRIC + self.acc.x += self.vel.x * self.game_resources.FRIC self.vel += self.acc self.pos += self.vel + 0.5 * self.acc @@ -215,7 +226,7 @@ class Player(Entity): ) def update(self): - hits = pygame.sprite.spritecollide(self, platforms, False) + hits = pygame.sprite.spritecollide(self, self.game_resources.platforms, False) if hits: if self.vel.y > 0: self.pos.y = hits[0].rect.top @@ -223,7 +234,7 @@ class Player(Entity): self.jumping = False if self.invulnerable: - self.invulnerable_timer += 1 / FPS + self.invulnerable_timer += 1 / self.game_resources.FPS if self.invulnerable_timer >= self.invulnerable_duration: self.invulnerable = False self.invulnerable_timer = 0 @@ -249,14 +260,20 @@ class Player(Entity): def draw_lives(self, surface): """Draws the player's remaining lives as icons in the top right corner.""" spacing = 5 - start_x = surface.get_width() - (self.max_lives * (life_icon_width + spacing)) + start_x = surface.get_width() - ( + self.max_lives * (self.game_resources.life_icon_width + spacing) + ) start_y = 10 for i in range(self.max_lives): if i < self.lives: # Vie active: afficher l'icône normale surface.blit( - self.life_icon, (start_x + i * (life_icon_width + spacing), start_y) + self.life_icon, + ( + start_x + i * (self.game_resources.life_icon_width + spacing), + start_y, + ), ) else: # Vie perdue: afficher l'icône grisée @@ -268,5 +285,9 @@ class Player(Entity): gray = (color[0] + color[1] + color[2]) // 3 grayscale_icon.set_at((x, y), (gray, gray, gray, color[3])) surface.blit( - grayscale_icon, (start_x + i * (life_icon_width + spacing), start_y) + grayscale_icon, + ( + start_x + i * (self.game_resources.life_icon_width + spacing), + start_y, + ), ) diff --git a/src/Map/parser.py b/src/Map/parser.py index 8bc0e3d..14c170b 100644 --- a/src/Map/parser.py +++ b/src/Map/parser.py @@ -5,13 +5,13 @@ from src.Entity.Platform import Platform from src.Entity.Player import Player from src.Entity.Enemy import Enemy from src.Entity.Checkpoint import Checkpoint -from src.constant import WIDTH, HEIGHT, all_sprites, platforms class MapParser: - def __init__(self): - self.all_sprites = all_sprites - self.platforms = platforms + def __init__(self, game_resources): + self.game_resources = game_resources + self.all_sprites = self.game_resources.all_sprites + self.platforms = self.game_resources.platforms self.enemies = pygame.sprite.Group() self.collectibles = pygame.sprite.Group() self.checkpoints = pygame.sprite.Group() @@ -34,8 +34,8 @@ class MapParser: "collectibles": self.collectibles, "map_properties": { "name": map_data.get("name", "Unnamed Level"), - "width": map_data.get("width", WIDTH), - "height": map_data.get("height", HEIGHT), + "width": map_data.get("width", self.game_resources.WIDTH), + "height": map_data.get("height", self.game_resources.HEIGHT), }, "checkpoints": self.checkpoints, } @@ -105,7 +105,7 @@ class MapParser: # Create player at spawn point spawn = map_data.get("spawn_point", {"x": 50, "y": 700}) - self.player = Player() + self.player = Player(self.game_resources) self.player.pos.x = spawn["x"] self.player.pos.y = spawn["y"] self.all_sprites.add(self.player) @@ -125,12 +125,12 @@ class MapParser: # Create background image if "background" in map_data: - print(f"Loading background image: {map_data['background']}") if os.path.isfile(map_data["background"]): background = pygame.image.load(map_data["background"]).convert_alpha() - background = pygame.transform.scale(background, (WIDTH, HEIGHT)) + background = pygame.transform.scale( + background, (self.game_resources.WIDTH, self.game_resources.HEIGHT) + ) self.background = background - print("Background image loaded") else: print(f"Background image not found: {map_data['background']}") else: diff --git a/src/Menu/Button.py b/src/Menu/Button.py index 3d4c2dd..e735df5 100644 --- a/src/Menu/Button.py +++ b/src/Menu/Button.py @@ -1,6 +1,5 @@ from pygame.locals import * import pygame -from src.constant import font class Button: @@ -13,7 +12,7 @@ class Button: self.action = action self.hover = False - def draw(self, surface): + def draw(self, surface, font): # Button colors color = (100, 149, 237) if self.hover else (65, 105, 225) border_color = (255, 255, 255) diff --git a/src/Menu/Leaderboard.py b/src/Menu/Leaderboard.py index 8dc69da..f6a0d51 100644 --- a/src/Menu/Leaderboard.py +++ b/src/Menu/Leaderboard.py @@ -1,21 +1,25 @@ import pygame -from src.constant import WIDTH, HEIGHT, font from src.Menu.Button import Button class Leaderboard: - def __init__(self): + def __init__(self, HEIGHT, WIDTH, font): + self.HEIGHT = HEIGHT + self.WIDTH = WIDTH + self.font = font self.tabs = ["Mode Normal", "Mode Infini"] self.current_tab = 0 self.scores = { 0: [("Player1", 1000), ("Player2", 800), ("Player3", 600)], 1: [("Player1", 2000), ("Player2", 1500), ("Player3", 1200)], } - self.back_button = Button("Retour", 20, HEIGHT - 70, 120, 50, "menu") + self.back_button = Button("Retour", 20, self.HEIGHT - 70, 120, 50, "menu") tab_width = 150 self.tab_buttons = [ - Button(self.tabs[0], WIDTH // 2 - tab_width, 80, tab_width, 40, "tab_0"), - Button(self.tabs[1], WIDTH // 2, 80, tab_width, 40, "tab_1"), + Button( + self.tabs[0], self.WIDTH // 2 - tab_width, 80, tab_width, 40, "tab_0" + ), + Button(self.tabs[1], self.WIDTH // 2, 80, tab_width, 40, "tab_1"), ] def draw(self, surface): @@ -23,7 +27,7 @@ class Leaderboard: title = pygame.font.SysFont("Arial", 48).render( "Classement", True, (0, 191, 255) ) - title_rect = title.get_rect(center=(WIDTH // 2, 40)) + title_rect = title.get_rect(center=(self.WIDTH // 2, 40)) surface.blit(title, title_rect) # Draw tabs @@ -39,8 +43,12 @@ class Leaderboard: # Draw scores y_pos = 150 for i, (name, score) in enumerate(self.scores[self.current_tab]): - rank_text = font.render(f"{i+1}. {name}: {score}", True, (255, 255, 255)) - surface.blit(rank_text, (WIDTH // 2 - rank_text.get_width() // 2, y_pos)) + rank_text = self.font.render( + f"{i+1}. {name}: {score}", True, (255, 255, 255) + ) + surface.blit( + rank_text, (self.WIDTH // 2 - rank_text.get_width() // 2, y_pos) + ) y_pos += 40 self.back_button.draw(surface) diff --git a/src/Menu/Menu.py b/src/Menu/Menu.py index 52a02e8..1fac3eb 100644 --- a/src/Menu/Menu.py +++ b/src/Menu/Menu.py @@ -1,21 +1,21 @@ import pygame -from src.constant import HEIGHT, WIDTH from src.Menu.Button import Button class Menu: - def __init__(self): + def __init__(self, game_resources): + self.game_resources = game_resources self.buttons = [] button_width = 250 button_height = 60 button_spacing = 20 - start_y = HEIGHT // 2 - 100 + start_y = self.game_resources.HEIGHT // 2 - 100 # Create buttons centered horizontally self.buttons.append( Button( "Jouer", - WIDTH // 2 - button_width // 2, + self.game_resources.WIDTH // 2 - button_width // 2, start_y, button_width, button_height, @@ -27,7 +27,7 @@ class Menu: self.buttons.append( Button( "Jouer en mode infini", - WIDTH // 2 - button_width // 2, + self.game_resources.WIDTH // 2 - button_width // 2, start_y, button_width, button_height, @@ -39,7 +39,7 @@ class Menu: self.buttons.append( Button( "Classement", - WIDTH // 2 - button_width // 2, + self.game_resources.WIDTH // 2 - button_width // 2, start_y, button_width, button_height, @@ -51,7 +51,7 @@ class Menu: self.buttons.append( Button( "Quitter", - WIDTH // 2 - button_width // 2, + self.game_resources.WIDTH // 2 - button_width // 2, start_y, button_width, button_height, @@ -64,12 +64,14 @@ class Menu: title = pygame.font.SysFont("Arial", 72).render( "Project Sanic", True, (0, 191, 255) ) - title_rect = title.get_rect(center=(WIDTH // 2, HEIGHT // 4)) + title_rect = title.get_rect( + center=(self.game_resources.WIDTH // 2, self.game_resources.HEIGHT // 4) + ) surface.blit(title, title_rect) # Draw buttons for button in self.buttons: - button.draw(surface) + button.draw(surface, self.game_resources.font) def handle_event(self, event): for button in self.buttons: diff --git a/src/constant.py b/src/constant.py index c008544..fba4fc2 100644 --- a/src/constant.py +++ b/src/constant.py @@ -1,24 +1,33 @@ import pygame -pygame.init() -FPS = 60 -ACC = 0.5 -FRIC = -0.12 -WIDTH = 1200 -HEIGHT = 800 -platforms = pygame.sprite.Group() -vec = pygame.math.Vector2 -displaysurface = pygame.display.set_mode((WIDTH, HEIGHT), pygame.RESIZABLE) -pygame.display.set_caption("Project Sanic") -FramePerSec = pygame.time.Clock() -all_sprites = pygame.sprite.Group() -fullscreen = False -ORIGINAL_WIDTH = WIDTH -ORIGINAL_HEIGHT = HEIGHT -life_icon_width = 50 +class GameResources: + def __init__(self): + pygame.init() -try: - font = pygame.font.SysFont("Arial", 20) -except: - font = pygame.font.Font(None, 20) + # Constantes + self.FPS = 60 + self.ACC = 0.5 + self.FRIC = -0.12 + self.WIDTH = 1200 + self.HEIGHT = 800 + self.ORIGINAL_WIDTH = self.WIDTH + self.ORIGINAL_HEIGHT = self.HEIGHT + self.life_icon_width = 50 + self.fullscreen = False + + # Ressources + self.platforms = pygame.sprite.Group() + self.vec = pygame.math.Vector2 + self.displaysurface = pygame.display.set_mode( + (self.WIDTH, self.HEIGHT), pygame.RESIZABLE + ) + pygame.display.set_caption("Project Sanic") + self.FramePerSec = pygame.time.Clock() + self.all_sprites = pygame.sprite.Group() + + # Font + try: + self.font = pygame.font.SysFont("Arial", 20) + except: + self.font = pygame.font.Font(None, 20) diff --git a/src/game.py b/src/game.py index 7300276..a651b6f 100644 --- a/src/game.py +++ b/src/game.py @@ -3,83 +3,77 @@ import sys from pygame.locals import * from src.Entity.Platform import Platform from src.Entity.Player import Player -from src.constant import ( - displaysurface, - FramePerSec, - font, - FPS, - platforms, - all_sprites, - vec, -) from src.Map.parser import MapParser from src.Database.CheckpointDB import CheckpointDB -def initialize_game(map_file="map_test.json"): +def initialize_game(game_resources, map_file="map_test.json"): """Initialize game with map from JSON file""" - parser = MapParser() + parser = MapParser(game_resources) map_objects = parser.load_map(map_file) if not map_objects: # Fallback to default setup if map loading fails - platforms.empty() - all_sprites.empty() + game_resources.platforms.empty() + game_resources.all_sprites.empty() PT1 = Platform(1200, 20, 600, 400) - P1 = Player() + P1 = Player(game_resources) - platforms.add(PT1) - all_sprites.add(PT1) - all_sprites.add(P1) + game_resources.platforms.add(PT1) + game_resources.all_sprites.add(PT1) + game_resources.all_sprites.add(P1) - return P1, PT1, platforms, all_sprites, None # Return None for background + return ( + P1, + PT1, + game_resources.platforms, + game_resources.all_sprites, + None, + None, + ) return ( map_objects["player"], - None, # No specific platform reference needed + None, map_objects["platforms"], map_objects["all_sprites"], - parser.background, # Return the loaded background + parser.background, map_objects["checkpoints"], ) -def reset_game(): +def reset_game(game_resources): """Reset the game to initial state""" - global platforms, all_sprites, camera - - # Empty all sprite groups - platforms.empty() - all_sprites.empty() - # Reload game objects player, _, platforms, all_sprites, background, checkpoints = initialize_game( - "map_test.json" + game_resources, "map_test.json" ) return player, platforms, all_sprites, background, checkpoints -def reset_game_with_checkpoint(map_name="map_test.json"): +def reset_game_with_checkpoint(map_name, game_resources): """ Reset the game and respawn player at checkpoint if available Args: map_name: Name of the current map + game_resources: GameResources object """ - # Initialize game normally - player, platforms, all_sprites, background, checkpoints = reset_game() - - # Check if there's a saved checkpoint + # Check the checkpoint database for saved checkpoint db = CheckpointDB() checkpoint_pos = db.get_checkpoint(map_name) - # If checkpoint exists, teleport player there + # Initialize game + player, _, platforms, all_sprites, background, checkpoints = initialize_game( + game_resources, map_name + ) + + # If checkpoint exists, respawn player at checkpoint if checkpoint_pos: - player.pos = vec(checkpoint_pos[0], checkpoint_pos[1]) + player.pos = game_resources.vec(checkpoint_pos[0], checkpoint_pos[1]) player.update_rect() - print(f"Player respawned at checkpoint: {checkpoint_pos}") return player, platforms, all_sprites, background, checkpoints