Merge pull request #32 from BreizhHardware/dev_felix

Feat(Game Logic) - Integrate speedrun timer functionality; add Speedr…
This commit is contained in:
Clément Hervouet
2025-04-09 11:07:40 +02:00
committed by GitHub
4 changed files with 283 additions and 41 deletions

View File

@@ -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

View File

@@ -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))

View File

@@ -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

View File

@@ -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