Feat(Infinite Mode) - Implement infinite level generation and management; add InfiniteMapGenerator and InfiniteMapManager for procedural map creation

This commit is contained in:
Félix MARQUET
2025-04-03 14:24:27 +02:00
parent 90b6c948eb
commit d34b5324dc
8 changed files with 826 additions and 18 deletions

BIN
assets/sound/Death.mp3 Normal file

Binary file not shown.

285
map/infinite/2e9b8d03.json Normal file
View File

@@ -0,0 +1,285 @@
{
"name": "Niveau Infini 1",
"width": 2400,
"height": 800,
"background": "assets/map/background/desert_bg.jpg",
"gravity": 1.0,
"platforms": [
{
"id": "platform_start",
"x": 180,
"y": 260,
"width": 540,
"height": 60,
"texture": "assets/map/platform/stone_texture.jpg",
"is_moving": false
},
{
"id": "platform2",
"x": 694,
"y": 259,
"width": 241,
"height": 40,
"texture": "assets/map/platform/wood_texture.jpg",
"is_moving": false
},
{
"id": "platform3",
"x": 1080,
"y": 167,
"width": 163,
"height": 20,
"texture": "assets/map/platform/grass_texture.jpg",
"is_moving": false
},
{
"id": "platform4",
"x": 1399,
"y": 255,
"width": 184,
"height": 60,
"texture": "assets/map/platform/stone_texture.jpg",
"is_moving": true,
"movement": {
"type": "linear",
"points": [
{
"x": 1399,
"y": 255
},
{
"x": 1539,
"y": 255
}
],
"speed": 2,
"wait_time": 0.5
}
},
{
"id": "platform5",
"x": 1684,
"y": 189,
"width": 197,
"height": 20,
"texture": "assets/map/platform/grass_texture.jpg",
"is_moving": false
},
{
"id": "platform6",
"x": 2030,
"y": 337,
"width": 162,
"height": 60,
"texture": "assets/map/platform/grass_texture.jpg",
"is_moving": true,
"movement": {
"type": "linear",
"points": [
{
"x": 2030,
"y": 337
},
{
"x": 2030,
"y": 462
}
],
"speed": 1,
"wait_time": 0.5
}
},
{
"id": "platform7",
"x": 2291,
"y": 241,
"width": 234,
"height": 20,
"texture": "assets/map/platform/wood_texture.jpg",
"is_moving": true,
"movement": {
"type": "linear",
"points": [
{
"x": 2291,
"y": 241
},
{
"x": 2291,
"y": 395
}
],
"speed": 3,
"wait_time": 0.5
}
},
{
"id": "platform8",
"x": 2671,
"y": 336,
"width": 243,
"height": 20,
"texture": "assets/map/platform/wood_texture.jpg",
"is_moving": false
},
{
"id": "platform9",
"x": 3021,
"y": 388,
"width": 233,
"height": 60,
"texture": "assets/map/platform/stone_texture.jpg",
"is_moving": true,
"movement": {
"type": "linear",
"points": [
{
"x": 3021,
"y": 388
},
{
"x": 3021,
"y": 498
}
],
"speed": 1,
"wait_time": 0.5
}
},
{
"id": "platform10",
"x": 3380,
"y": 347,
"width": 198,
"height": 20,
"texture": "assets/map/platform/stone_texture.jpg",
"is_moving": true,
"movement": {
"type": "linear",
"points": [
{
"x": 3380,
"y": 347
},
{
"x": 3380,
"y": 475
}
],
"speed": 2,
"wait_time": 0.5
}
},
{
"id": "platform11",
"x": 3704,
"y": 394,
"width": 218,
"height": 20,
"texture": "assets/map/platform/grass_texture.jpg",
"is_moving": true,
"movement": {
"type": "linear",
"points": [
{
"x": 3704,
"y": 394
},
{
"x": 3826,
"y": 394
}
],
"speed": 2,
"wait_time": 0.5
}
},
{
"id": "platform12",
"x": 4059,
"y": 198,
"width": 270,
"height": 40,
"texture": "assets/map/platform/grass_texture.jpg",
"is_moving": false
},
{
"id": "platform13",
"x": 4426,
"y": 254,
"width": 156,
"height": 40,
"texture": "assets/map/platform/wood_texture.jpg",
"is_moving": false
}
],
"enemies": [
{
"id": "enemy1",
"type": "turret",
"x": 951,
"y": 376,
"patrol_distance": 291
},
{
"id": "enemy2",
"type": "turret",
"x": 2173,
"y": 377,
"patrol_distance": 150
}
],
"checkpoints": [],
"exits": [
{
"x": 2300,
"y": 200,
"width": 50,
"height": 80,
"next_level": "NEXT_INFINITE_LEVEL",
"sprite": "assets/map/exit/door.png"
}
],
"collectibles": [
{
"id": "collectible1",
"type": "shield",
"x": 1013,
"y": 101
},
{
"id": "collectible2",
"type": "shield",
"x": 1749,
"y": 183
},
{
"id": "collectible3",
"type": "coin",
"x": 2070,
"y": 196
},
{
"id": "collectible4",
"type": "health",
"x": 758,
"y": 375
},
{
"id": "collectible5",
"type": "shield",
"x": 681,
"y": 353
},
{
"id": "collectible6",
"type": "shield",
"x": 581,
"y": 163
}
],
"spawn_point": {
"x": 260.0,
"y": 200.0
}
}

225
map/infinite/822377c7.json Normal file
View File

@@ -0,0 +1,225 @@
{
"name": "Niveau Infini 1",
"width": 2400,
"height": 800,
"background": "assets/map/background/desert_bg.jpg",
"gravity": 1.0,
"platforms": [
{
"id": "platform_start",
"x": 180,
"y": 260,
"width": 540,
"height": 60,
"texture": "assets/map/platform/stone_texture.jpg",
"is_moving": false
},
{
"id": "platform2",
"x": 762,
"y": 316,
"width": 238,
"height": 20,
"texture": "assets/map/platform/wood_texture.jpg",
"is_moving": false
},
{
"id": "platform3",
"x": 1184,
"y": 317,
"width": 131,
"height": 40,
"texture": "assets/map/platform/stone_texture.jpg",
"is_moving": true,
"movement": {
"type": "linear",
"points": [
{
"x": 1184,
"y": 317
},
{
"x": 1326,
"y": 317
}
],
"speed": 1,
"wait_time": 0.5
}
},
{
"id": "platform4",
"x": 1400,
"y": 225,
"width": 256,
"height": 40,
"texture": "assets/map/platform/stone_texture.jpg",
"is_moving": false
},
{
"id": "platform5",
"x": 1736,
"y": 282,
"width": 178,
"height": 60,
"texture": "assets/map/platform/grass_texture.jpg",
"is_moving": false
},
{
"id": "platform6",
"x": 2025,
"y": 170,
"width": 159,
"height": 40,
"texture": "assets/map/platform/stone_texture.jpg",
"is_moving": false
},
{
"id": "platform7",
"x": 2347,
"y": 367,
"width": 150,
"height": 20,
"texture": "assets/map/platform/wood_texture.jpg",
"is_moving": false
},
{
"id": "platform8",
"x": 2632,
"y": 385,
"width": 256,
"height": 60,
"texture": "assets/map/platform/grass_texture.jpg",
"is_moving": false
},
{
"id": "platform9",
"x": 2984,
"y": 227,
"width": 221,
"height": 20,
"texture": "assets/map/platform/grass_texture.jpg",
"is_moving": false
},
{
"id": "platform10",
"x": 3326,
"y": 313,
"width": 273,
"height": 40,
"texture": "assets/map/platform/stone_texture.jpg",
"is_moving": false
},
{
"id": "platform11",
"x": 3715,
"y": 330,
"width": 189,
"height": 20,
"texture": "assets/map/platform/grass_texture.jpg",
"is_moving": false
},
{
"id": "platform12",
"x": 4007,
"y": 304,
"width": 112,
"height": 60,
"texture": "assets/map/platform/wood_texture.jpg",
"is_moving": true,
"movement": {
"type": "linear",
"points": [
{
"x": 4007,
"y": 304
},
{
"x": 4007,
"y": 407
}
],
"speed": 3,
"wait_time": 0.5
}
},
{
"id": "platform13",
"x": 4216,
"y": 383,
"width": 285,
"height": 20,
"texture": "assets/map/platform/grass_texture.jpg",
"is_moving": false
}
],
"enemies": [
{
"id": "enemy1",
"type": "turret",
"x": 776,
"y": 356,
"patrol_distance": 121
},
{
"id": "enemy2",
"type": "walker",
"x": 1080,
"y": 305,
"patrol_distance": 254
}
],
"checkpoints": [],
"exits": [
{
"x": 2300,
"y": 200,
"width": 50,
"height": 80,
"next_level": "map/infinite/2e9b8d03.json",
"sprite": "assets/map/exit/door.png"
}
],
"collectibles": [
{
"id": "collectible1",
"type": "health",
"x": 1672,
"y": 119
},
{
"id": "collectible2",
"type": "coin",
"x": 775,
"y": 251
},
{
"id": "collectible3",
"type": "health",
"x": 814,
"y": 207
},
{
"id": "collectible4",
"type": "coin",
"x": 1578,
"y": 278
},
{
"id": "collectible5",
"type": "health",
"x": 2069,
"y": 209
},
{
"id": "collectible6",
"type": "shield",
"x": 1163,
"y": 304
}
],
"spawn_point": {
"x": 260.0,
"y": 200.0
}
}

View File

@@ -0,0 +1,157 @@
import random
import json
import os
import uuid
class InfiniteMapGenerator:
"""Procedural map generator for infinite levels."""
def __init__(self, game_resources):
self.game_resources = game_resources
self.width = 2400
self.height = 800
self.backgrounds = [
"assets/map/background/forest_bg.jpg",
"assets/map/background/desert_bg.jpg",
"assets/map/background/mountain_bg.jpg",
]
self.platform_textures = [
"assets/map/platform/grass_texture.jpg",
"assets/map/platform/stone_texture.jpg",
"assets/map/platform/wood_texture.jpg",
]
# Create the directory for infinite maps if it doesn't exist
os.makedirs("map/infinite", exist_ok=True)
def generate_map(self, difficulty=1):
"""Generate a new infinite map with the specified difficulty level."""
map_data = {
"name": f"Niveau Infini {difficulty}",
"width": self.width,
"height": self.height,
"background": random.choice(self.backgrounds),
"gravity": 1.0,
"platforms": self._generate_platforms(difficulty),
"enemies": self._generate_enemies(difficulty),
"checkpoints": [],
"exits": [self._generate_exit()],
"collectibles": self._generate_collectibles(difficulty),
"spawn_point": {"x": 260.0, "y": 200.0},
}
# Save the map data to a JSON file
map_id = str(uuid.uuid4())[:8]
map_path = f"map/infinite/{map_id}.json"
with open(map_path, "w") as f:
json.dump(map_data, f, indent=2)
return map_path
def _generate_platforms(self, difficulty):
platforms = []
# Starting platform
platforms.append(
{
"id": "platform_start",
"x": 180,
"y": 260,
"width": 540,
"height": 60,
"texture": random.choice(self.platform_textures),
"is_moving": False,
}
)
# Generate additional platforms
num_platforms = 10 + difficulty * 2
last_x = 600
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)
is_moving = random.random() < min(0.1 + difficulty * 0.05, 0.5)
platform = {
"id": f"platform{i+2}",
"x": x,
"y": y,
"width": width,
"height": random.choice([20, 40, 60]),
"texture": random.choice(self.platform_textures),
"is_moving": is_moving,
}
if is_moving:
move_direction = random.choice(["horizontal", "vertical"])
distance = random.randint(100, 200)
if move_direction == "horizontal":
platform["movement"] = {
"type": "linear",
"points": [{"x": x, "y": y}, {"x": x + distance, "y": y}],
"speed": random.randint(1, 3),
"wait_time": 0.5,
}
else:
platform["movement"] = {
"type": "linear",
"points": [{"x": x, "y": y}, {"x": x, "y": y + distance}],
"speed": random.randint(1, 3),
"wait_time": 0.5,
}
platforms.append(platform)
last_x = x + width
return platforms
def _generate_enemies(self, difficulty):
enemies = []
num_enemies = difficulty * 2
enemy_types = ["walker", "flyer", "turret"]
for i in range(num_enemies):
enemy = {
"id": f"enemy{i+1}",
"type": random.choice(enemy_types),
"x": random.randint(600, self.width - 200),
"y": random.randint(100, 400),
"patrol_distance": random.randint(100, 300),
}
enemies.append(enemy)
return enemies
def _generate_collectibles(self, difficulty):
collectibles = []
num_collectibles = 5 + difficulty
collectible_types = ["coin", "health", "shield"]
for i in range(num_collectibles):
collectible = {
"id": f"collectible{i+1}",
"type": random.choice(collectible_types),
"x": random.randint(400, self.width - 100),
"y": random.randint(100, 400),
}
collectibles.append(collectible)
return collectibles
def _generate_exit(self):
return {
"x": self.width - 100,
"y": 200,
"width": 50,
"height": 80,
"next_level": "NEXT_INFINITE_LEVEL",
"sprite": "assets/map/exit/door.png",
}

View File

@@ -0,0 +1,82 @@
import os
import json
import glob
from src.Map.Infinite.InfiniteMapGenerator import InfiniteMapGenerator
class InfiniteMapManager:
"""Handle infinite map generation and management."""
def __init__(self, game_resources):
self.game_resources = game_resources
self.map_generator = InfiniteMapGenerator(game_resources)
self.current_level = 0
self.active_maps = []
self.difficulty = 1
def start_infinite_mode(self):
"""Start the infinite mode by generating the first two maps."""
self._clean_old_maps()
# Generate the first two maps
first_map = self.map_generator.generate_map(difficulty=self.difficulty)
second_map = self.map_generator.generate_map(difficulty=self.difficulty)
# Configure the first map to point to the second map
self._update_exit_target(first_map, second_map)
self.active_maps = [first_map, second_map]
self.current_level = 1
return first_map
def advance_to_next_level(self):
"""Progress to the next level in infinite mode and delete the previous one."""
# Delete the oldest map
if self.active_maps:
old_map = self.active_maps.pop(0)
try:
os.remove(old_map)
except:
print(f"Erreur: Impossible de supprimer {old_map}")
# Up the difficulty every 3 levels
self.current_level += 1
if self.current_level % 3 == 0:
self.difficulty = min(10, self.difficulty + 1)
# Generate a new map
new_map = self.map_generator.generate_map(difficulty=self.difficulty)
# Update the exit target of the last map to point to the new one
if self.active_maps:
self._update_exit_target(self.active_maps[0], new_map)
self.active_maps.append(new_map)
return self.active_maps[0]
def _update_exit_target(self, map_path, next_map_path):
"""Update the exit of the current map to point to the next map."""
try:
with open(map_path, "r") as f:
map_data = json.load(f)
if "exits" in map_data and map_data["exits"]:
for exit_obj in map_data["exits"]:
exit_obj["next_level"] = next_map_path
with open(map_path, "w") as f:
json.dump(map_data, f, indent=2)
except Exception as e:
print(f"Error while updating exit: {e}")
def _clean_old_maps(self):
"""Delete all old infinite maps."""
map_files = glob.glob("map/infinite/*.json")
for file in map_files:
try:
os.remove(file)
except:
print(f"Error: Unable to delete {file}")

View File

@@ -27,6 +27,9 @@ class GameResources:
pygame.display.set_caption("Project Sanic")
self.FramePerSec = pygame.time.Clock()
self.infinite_manager = None
self.infinite_mode = False
# Font
try:
self.font = pygame.font.SysFont("Arial", 20)

View File

@@ -9,6 +9,7 @@ from src.Entity.Platform import Platform
from src.Entity.Player import Player
from src.Map.parser import MapParser
from src.Database.CheckpointDB import CheckpointDB
from src.Map.Infinite.InfiniteMapManager import InfiniteMapManager
def initialize_game(game_resources, map_file="map/levels/1.json"):
@@ -53,12 +54,17 @@ def initialize_game(game_resources, map_file="map/levels/1.json"):
for exit_obj in exits:
exit_obj.set_player(map_objects["player"])
background = map_objects.get("background", None)
if background is None:
background = pygame.Surface((game_resources.WIDTH, game_resources.HEIGHT))
background.fill((0, 0, 0))
return (
map_objects["player"],
None,
map_objects["platforms"],
map_objects["all_sprites"],
parser.background,
background,
map_objects["checkpoints"],
exits,
)
@@ -117,5 +123,31 @@ def clear_level_progress():
print(f"Error clearing level progress: {e}")
def start_infinite_mode(game_resources):
"""Start the infinite mode of the game"""
# Create a new InfiniteMapManager
infinite_manager = InfiniteMapManager(game_resources)
game_resources.infinite_manager = infinite_manager
game_resources.infinite_mode = True
# Generate the first level
first_level = infinite_manager.start_infinite_mode()
# Initialize the game with the generated level
return initialize_game(game_resources, first_level)
def handle_exit_collision(exit_obj, game_resources, level_file):
"""Handle exit collision and transition to next level, including infinite mode"""
next_level = exit_obj.next_level
# Mod infinite: if the next level is "NEXT_INFINITE_LEVEL", generate a new level
if hasattr(game_resources, "infinite_mode") and game_resources.infinite_mode:
if next_level == "NEXT_INFINITE_LEVEL":
next_level = game_resources.infinite_manager.advance_to_next_level()
return initialize_game(game_resources, next_level)
if __name__ == "__main__":
print("Please run the game using main.py")

View File

@@ -12,6 +12,8 @@ from src.game import (
initialize_game,
reset_game_with_checkpoint,
clear_checkpoint_database,
start_infinite_mode,
handle_exit_collision,
)
from src.constant import GameResources
from src.Menu.Menu import Menu
@@ -56,6 +58,15 @@ def handler():
print(f"Erreur de chargement de l'image: {e}")
death_image = None
try:
death_sound = pygame.mixer.Sound("assets/sound/Death.mp3")
death_display_time = death_sound.get_length()
print(f"Son Death.mp3 chargé avec succès, durée: {death_display_time} secondes")
except Exception as e:
print(f"Erreur de chargement du son Death.mp3: {e}")
death_sound = None
death_display_time = 2
# Initialize game state and objects
current_state = MENU
main_menu = Menu(game_resources)
@@ -131,6 +142,8 @@ def handler():
if event.dict.get("action") == "player_death":
current_state = DEATH_SCREEN
death_timer = 0
if death_sound:
death_sound.play()
db = CheckpointDB()
checkpoint_data = db.get_checkpoint(level_file)
@@ -323,20 +336,30 @@ def handler():
exits_hit = pygame.sprite.spritecollide(P1, exits, False) if exits else []
for exit in exits_hit:
current_level_match = re.search(r"(\d+)\.json$", level_file)
if current_level_match:
current_level = int(current_level_match.group(1))
next_level = current_level + 1
if (
hasattr(game_resources, "infinite_mode")
and game_resources.infinite_mode
):
# Mod infinit : load the next level without the menu
P1, P1T, platforms, all_sprites, background, checkpoints, exits = (
handle_exit_collision(exit, game_resources, level_file)
)
else:
# Mod normal : unlock the next level and return to the menu
current_level_match = re.search(r"(\d+)\.json$", level_file)
if current_level_match:
current_level = int(current_level_match.group(1))
next_level = current_level + 1
# Unlock next level
db = LevelDB()
db.unlock_level(next_level)
db.close()
# Unlock next level
db = LevelDB()
db.unlock_level(next_level)
db.close()
# Return to level select menu
current_state = MENU
current_menu = "level_select"
level_select_menu = LevelSelectMenu(game_resources)
# Return to level select menu
current_state = MENU
current_menu = "level_select"
level_select_menu = LevelSelectMenu(game_resources)
# Display FPS and coordinates (fixed position UI elements)
fps = int(FramePerSec.get_fps())
@@ -354,22 +377,23 @@ def handler():
P1.draw_lives(displaysurface)
elif current_state == INFINITE:
# Placeholder for infinite mode
text = font.render("Mode Infini - À implémenter", True, (255, 255, 255))
displaysurface.blit(text, (WIDTH // 2 - text.get_width() // 2, HEIGHT // 2))
P1, P1T, platforms, all_sprites, background, checkpoints, exits = (
start_infinite_mode(game_resources)
)
current_state = PLAYING
elif current_state == LEADERBOARD:
leaderboard.draw(displaysurface)
elif current_state == DEATH_SCREEN:
displaysurface.fill((0, 0, 0)) # Fond rouge foncé
displaysurface.fill((0, 0, 0))
if death_image:
scaled_image = pygame.transform.scale(death_image, (WIDTH, HEIGHT))
image_rect = scaled_image.get_rect(center=(WIDTH // 2, HEIGHT // 2))
displaysurface.blit(scaled_image, image_rect)
# Gestion du timer
# Timer for death screen
death_timer += dt
if death_timer >= death_display_time:
if checkpoint_data: