From 60aa7f6e11acbd6c142c879279025ecd4ab40180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Wed, 9 Apr 2025 10:33:17 +0200 Subject: [PATCH] Feat(Game Logic) - Integrate speedrun timer functionality; add SpeedrunTimer class, modify game state management to include timer, and update leaderboard to display best times. --- src/Map/Infinite/InfiniteMapGenerator.py | 10 +- src/Map/Speedrun/SpeedrunTimer.py | 105 +++++++++++++++++ src/Menu/Leaderboard.py | 142 ++++++++++++++++++----- src/handler.py | 67 ++++++++--- 4 files changed, 283 insertions(+), 41 deletions(-) create mode 100644 src/Map/Speedrun/SpeedrunTimer.py diff --git a/src/Map/Infinite/InfiniteMapGenerator.py b/src/Map/Infinite/InfiniteMapGenerator.py index 54de8a2..4923692 100644 --- a/src/Map/Infinite/InfiniteMapGenerator.py +++ b/src/Map/Infinite/InfiniteMapGenerator.py @@ -70,13 +70,20 @@ class InfiniteMapGenerator: # Generate additional platforms num_platforms = 10 + difficulty * 2 last_x = 600 + last_y = 260 + max_jump_height = 120 + for i in range(num_platforms): width = random.randint( max(40, 100 - difficulty * 5), max(120, 300 - difficulty * 10) ) gap = random.randint(80, 200) x = last_x + gap - y = random.randint(150, 400) + + min_y = max(150, last_y - max_jump_height) + max_y = min(400, last_y + 80) + + y = random.randint(min_y, max_y) is_moving = random.random() < min(0.1 + difficulty * 0.05, 0.5) @@ -111,6 +118,7 @@ class InfiniteMapGenerator: platforms.append(platform) last_x = x + width + last_y = y return platforms diff --git a/src/Map/Speedrun/SpeedrunTimer.py b/src/Map/Speedrun/SpeedrunTimer.py new file mode 100644 index 0000000..0a39bd2 --- /dev/null +++ b/src/Map/Speedrun/SpeedrunTimer.py @@ -0,0 +1,105 @@ +import pygame +import time +import sqlite3 +from datetime import timedelta + + +class SpeedrunTimer: + def __init__(self, level_id, db_path="game.db"): + self.level_id = level_id + self.db_path = db_path + self.start_time = None + self.current_time = 0 + self.is_running = False + self.best_time = self._get_best_time() + self.color = (0, 255, 0) # Green by default + self.font = pygame.font.Font(None, 36) + + def start(self): + """Start the timer""" + self.start_time = time.time() + self.is_running = True + + def stop(self): + """Stop the timer and return the final time""" + if self.is_running: + self.current_time = time.time() - self.start_time + self.is_running = False + return self.current_time + return self.current_time + + def update(self): + """Update the timer and its color""" + if self.is_running: + self.current_time = time.time() - self.start_time + if self.best_time is not None: + if self.current_time < self.best_time: + self.color = (0, 255, 0) # Green if ahead + else: + self.color = (255, 0, 0) # Red if behind + + def save_time(self): + """Save the current time in the database""" + if not self.is_running and self.current_time > 0: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # Create the table if it doesn't exist + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS speedrun ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + level_id TEXT NOT NULL, + time REAL NOT NULL, + date TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """ + ) + + # Insert the new time + cursor.execute( + "INSERT INTO speedrun (level_id, time) VALUES (?, ?)", + (self.level_id, self.current_time), + ) + + conn.commit() + conn.close() + + # Update the best time + self.best_time = self._get_best_time() + + def _get_best_time(self): + """Get the best time for this level from the database""" + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute( + "SELECT MIN(time) FROM speedrun WHERE level_id = ?", (self.level_id,) + ) + + best_time = cursor.fetchone()[0] + conn.close() + + return best_time + except (sqlite3.Error, TypeError): + return None + + def format_time(self, time_value): + """Format the time in minutes:seconds.milliseconds""" + if time_value is None: + return "00:00.000" + + td = timedelta(seconds=time_value) + minutes, seconds = divmod(td.seconds, 60) + milliseconds = td.microseconds // 1000 + return f"{minutes:02}:{seconds:02}.{milliseconds:03}" + + def draw(self, surface): + """Display the timer on the screen""" + if self.level_id == "NEXT_INFINITE_LEVEL": + return # No timer for infinite mode + + formatted_time = self.format_time(self.current_time) + text = self.font.render(formatted_time, True, self.color) + surface.blit(text, (20, surface.get_height() - 50)) diff --git a/src/Menu/Leaderboard.py b/src/Menu/Leaderboard.py index aacc779..eedbfc4 100644 --- a/src/Menu/Leaderboard.py +++ b/src/Menu/Leaderboard.py @@ -1,29 +1,104 @@ import pygame +import sqlite3 +from datetime import datetime from src.Menu.Button import Button +from src.Database.LevelDB import LevelDB class Leaderboard: - def __init__(self, HEIGHT, WIDTH, font): - self.HEIGHT = HEIGHT + """This class represents the leaderboard menu for the game.""" + + def __init__(self, WIDTH, HEIGHT, font, db_path="game.db"): self.WIDTH = WIDTH + self.HEIGHT = HEIGHT self.font = font - self.tabs = ["Mode Normal", "Mode Infini"] + self.db_path = db_path + + self.levels = self.get_available_levels() + self.level_tabs = [f"Level {level}" for level in self.levels] + + # Define the tabs (levels + infinite mode) + self.tabs = self.level_tabs + ["Infinite mode"] 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, self.HEIGHT - 70, 120, 50, "menu") - tab_width = 150 - self.tab_buttons = [ - 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"), - ] + + self.back_button = Button("Back", 20, self.HEIGHT - 70, 120, 50, "menu") + + # Add buttons for each tab + self.tab_buttons = [] + tab_width = min(150, (self.WIDTH - 100) / len(self.tabs)) + + for i, tab_name in enumerate(self.tabs): + x_pos = 50 + i * tab_width + self.tab_buttons.append( + Button(tab_name, x_pos, 80, tab_width, 40, f"tab_{i}") + ) + + self.load_scores() + + def get_available_levels(self): + """Get the list of available levels from the database.""" + try: + db = LevelDB() + levels = db.get_all_unlocked_levels() + db.close() + return sorted(levels) + except: + return [1] + + def load_scores(self): + """Load scores from the database for each level.""" + self.scores = {} + + # Load scores for each level + for i, level in enumerate(self.levels): + self.scores[i] = self.get_level_scores(str(level)) + + # TO DO: Load scores for infinite mode + self.scores[len(self.levels)] = [] + + def get_level_scores(self, level_id): + """Get the top 10 scores for a specific level from the database.""" + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute( + """ + SELECT time, date + FROM speedrun + WHERE level_id = ? + ORDER BY time ASC + LIMIT 10 + """, + (level_id,), + ) + + results = cursor.fetchall() + conn.close() + + # Format results + formatted_results = [] + for time, date in results: + date_obj = datetime.strptime(date, "%Y-%m-%d %H:%M:%S") + formatted_date = date_obj.strftime("%d/%m/%Y") + formatted_results.append((formatted_date, time)) + + return formatted_results + except (sqlite3.Error, Exception) as e: + print(f"Error loading scores: {e}") + return [] + + def format_time(self, time_value): + """Format the time in minutes:seconds.milliseconds.""" + from datetime import timedelta + + td = timedelta(seconds=time_value) + minutes, seconds = divmod(td.seconds, 60) + milliseconds = int((time_value * 1000) % 1000) + return f"{minutes:02}:{seconds:02}.{milliseconds:03}" def draw(self, surface): - # Draw title + """Draw the leaderboard on the given surface.""" title = pygame.font.SysFont("Arial", 48).render( "Classement", True, (0, 191, 255) ) @@ -35,27 +110,42 @@ class Leaderboard: # Draw tabs for i, button in enumerate(self.tab_buttons): if i == self.current_tab: - pygame.draw.rect( - surface, - (100, 149, 237), - (button.x, button.y, button.width, button.height), - ) + # Highlight current tab + pygame.draw.rect(surface, (100, 100, 255), button.rect) button.draw(surface, font) # Draw scores y_pos = 150 - for i, (name, score) in enumerate(self.scores[self.current_tab]): - rank_text = self.font.render( - f"{i+1}. {name}: {score}", True, (255, 255, 255) + scores_for_tab = self.scores.get(self.current_tab, []) + + if not scores_for_tab: + no_scores_text = self.font.render( + "No time saved for this level", True, (255, 255, 255) ) surface.blit( - rank_text, (self.WIDTH // 2 - rank_text.get_width() // 2, y_pos) + no_scores_text, + (self.WIDTH // 2 - no_scores_text.get_width() // 2, y_pos + 40), ) - y_pos += 40 + else: + for i, (date, time) in enumerate(scores_for_tab): + + rank_text = self.font.render(f"{i+1}.", True, (255, 255, 255)) + surface.blit(rank_text, (self.WIDTH // 2 - 150, y_pos)) + + date_text = self.font.render(date, True, (255, 255, 255)) + surface.blit(date_text, (self.WIDTH // 2 - 100, y_pos)) + + time_text = self.font.render( + self.format_time(time), True, (255, 255, 255) + ) + surface.blit(time_text, (self.WIDTH // 2 + 50, y_pos)) + + y_pos += 40 self.back_button.draw(surface, font) def handle_event(self, event): + """Handle events for the leaderboard menu.""" action = self.back_button.handle_event(event) if action: return action diff --git a/src/handler.py b/src/handler.py index c1194f1..ff55a24 100644 --- a/src/handler.py +++ b/src/handler.py @@ -21,6 +21,7 @@ from src.Camera import Camera from src.Database.CheckpointDB import CheckpointDB from src.Map.Editor.LevelEditor import LevelEditor from src.Menu.LevelEditorSelectionMenu import LevelEditorSelectionMenu +from src.Map.Speedrun.SpeedrunTimer import SpeedrunTimer def initialize_game_resources(): @@ -207,6 +208,11 @@ def handle_menu_events( exits, collectibles, ) = initialize_game(game_resources, level_file) + + level_id = level_file.split("/")[-1].split(".")[0] + speedrun_timer = SpeedrunTimer(level_id) + speedrun_timer.start() + projectiles = pygame.sprite.Group() current_state = 1 # PLAYING return ( @@ -224,6 +230,7 @@ def handle_menu_events( collectibles, projectiles, editor_select_menu, + speedrun_timer, ) elif action == "open_editor": editor_select_menu = LevelEditorSelectionMenu(game_resources) @@ -243,6 +250,7 @@ def handle_menu_events( collectibles, None, editor_select_menu, + None, ) return ( @@ -260,6 +268,7 @@ def handle_menu_events( collectibles, None, editor_select_menu, + None, ) @@ -409,6 +418,7 @@ def draw_playing_state( game_resources, level_file, FramePerSec, + speedrun_timer=None, ): """Draw game state while playing""" # Draw background @@ -436,7 +446,7 @@ def draw_playing_state( checkpoint.activate() # Handle exit collisions - result = handle_exits(P1, exits, game_resources, level_file) + result = handle_exits(P1, exits, game_resources, level_file, speedrun_timer) # Handle collectibles coins_hit = pygame.sprite.spritecollide(P1, collectibles, False) @@ -445,18 +455,23 @@ def draw_playing_state( P1.collect_coin(displaysurface) # Draw UI elements - draw_ui_elements(displaysurface, P1, FramePerSec, font) + draw_ui_elements(displaysurface, P1, FramePerSec, font, speedrun_timer) return result -def handle_exits(P1, exits, game_resources, level_file): +def handle_exits(P1, exits, game_resources, level_file, speedrun_timer=None): """Handle collisions with level exits""" exits_hit = pygame.sprite.spritecollide(P1, exits, False) if exits else [] for exit in exits_hit: + if speedrun_timer: + speedrun_timer.stop() + speedrun_timer.save_time() + if hasattr(game_resources, "infinite_mode") and game_resources.infinite_mode: # Infinite mode: load the next level without going back to menu - return handle_exit_collision(exit, game_resources, level_file) + result = handle_exit_collision(exit, game_resources, level_file) + return {"action": "continue_infinite", "result": result} else: # Normal mode: unlock the next level and return to menu current_level_match = re.search(r"(\d+)\.json$", level_file) @@ -478,7 +493,7 @@ def handle_exits(P1, exits, game_resources, level_file): return None -def draw_ui_elements(displaysurface, P1, FramePerSec, font): +def draw_ui_elements(displaysurface, P1, FramePerSec, font, speedrun_timer=None): """Draw UI elements like FPS, player position, health, etc.""" # FPS counter fps = int(FramePerSec.get_fps()) @@ -497,6 +512,9 @@ def draw_ui_elements(displaysurface, P1, FramePerSec, font): P1.draw_coins(displaysurface) P1.draw_projectiles_amount(displaysurface) + if speedrun_timer: + speedrun_timer.draw(displaysurface) + def handle_death_screen( displaysurface, @@ -578,11 +596,12 @@ def handler(): leaderboard, projectiles, joysticks, - editor_select_menu, # Added editor_select_menu variable + editor_select_menu, ) = initialize_game_resources() # Initialize editor variables level_editor = None + speedrun_timer = None # Main game loop running = True @@ -646,7 +665,8 @@ def handler(): collectibles, ) = result[4:12] projectiles = result[12] - editor_select_menu = result[13] # Get the editor_select_menu + editor_select_menu = result[13] + speedrun_timer = result[14] elif current_state == LEADERBOARD: current_state = handle_leaderboard_events( @@ -726,6 +746,9 @@ def handler(): all_sprites, ) + if speedrun_timer: + speedrun_timer.update() + # Draw game state and process exit collisions exit_result = draw_playing_state( displaysurface, @@ -743,16 +766,32 @@ def handler(): game_resources, level_file, game_resources.FramePerSec, + speedrun_timer, ) # Handle level exit result - if ( - exit_result - and exit_result.get("action") == "return_to_level_select" - ): - current_state = exit_result["current_state"] - current_menu = exit_result["current_menu"] - level_select_menu = LevelSelectMenu(game_resources) + if exit_result: + if exit_result.get("action") == "return_to_level_select": + current_state = exit_result["current_state"] + current_menu = exit_result["current_menu"] + level_select_menu = LevelSelectMenu(game_resources) + elif exit_result.get("action") == "continue_infinite": + # Récupérer le résultat du handle_exit_collision + infinite_result = exit_result["result"] + # Utiliser le résultat pour continuer en mode infini + if infinite_result: + # Utiliser les valeurs retournées par handle_exit_collision + # Adapter selon la structure du tuple retourné + ( + P1, + PT1, + platforms, + all_sprites, + background, + checkpoints, + exits, + collectibles, + ) = infinite_result elif current_state == INFINITE: # Start infinite mode and switch to playing