mirror of
https://github.com/BreizhHardware/project_sanic.git
synced 2026-03-18 21:50:33 +01:00
Merge pull request #32 from BreizhHardware/dev_felix
Feat(Game Logic) - Integrate speedrun timer functionality; add Speedr…
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
105
src/Map/Speedrun/SpeedrunTimer.py
Normal file
105
src/Map/Speedrun/SpeedrunTimer.py
Normal 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))
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user