--- /dev/null
+**/add.mypy_cache
+orga.txt
+Project Management.xlsx
+cell.txt
+.venv
+mazegeneratordemerde.txt
+a.txt
+**/__pycache__
\ No newline at end of file
--- /dev/null
+*This project has been created as part of the 42 curriculum by agilliar and trimelen*
--- /dev/null
+{
+ "highscore_filename": "score.json",
+ "levels": 10,
+ "width": 20,
+ "height": 20,
+ "lives": 3,
+ "pacgum": 80,
+ "points_per_pacgum": 1,
+ "points_per_super_pacgum": 5,
+ "points_per_ghost": 20,
+ "time": 180,
+ "cheat_mod": false
+}
\ No newline at end of file
--- /dev/null
+[
+ {
+ "name": "Garobert",
+ "score": 1
+ },
+ {
+ "name": "ssss",
+ "score": 0
+ },
+ {
+ "name": "player",
+ "score": 51
+ },
+ {
+ "name": "dd",
+ "score": 61
+ },
+ {
+ "name": "",
+ "score": 0
+ },
+ {
+ "name": "kkk",
+ "score": 0
+ }
+]
\ No newline at end of file
--- /dev/null
+from .game import Game
+from .config import load_config
+from .error import error
+import importlib
+import sys
+
+
+for lib in ["pydantic", "mazegenerator", "pygame"]:
+ try:
+ importlib.import_module(lib)
+ except ModuleNotFoundError:
+ error("import missing, use Make install")
+
+try:
+ file_path = sys.argv[1]
+except IndexError:
+ file_path = ""
+config = load_config(file_path)
+try:
+ game = Game(config)
+except KeyboardInterrupt:
+ error("keyboardInterrupt.")
--- /dev/null
+from mazegenerator import MazeGenerator # type: ignore[import-untyped]
+from abc import ABC
+from .config import Configuration
+from .player import Player
+import random
+
+
+class Collectibles(ABC):
+ def __init__(self, point: int, size: int) -> None:
+ self.point = point
+ self.size = size
+
+ def collect(self) -> int:
+ return self.point
+
+
+class Pacgum(Collectibles):
+ def __init__(self, config: Configuration):
+ point = config.points_per_pacgum
+ super().__init__(point, 3)
+
+
+class SuperPacgum(Collectibles):
+ def __init__(self, config: Configuration):
+ point = config.points_per_pacgum
+ super().__init__(point, 6)
+
+
+class CollectableManager:
+ def __init__(self) -> None:
+ self.collectibles: dict[tuple[int, int], Collectibles] = {}
+
+ def fill(self, config: Configuration, maze: MazeGenerator,
+ player: Player) -> None:
+ collectibles: dict[tuple[int, int], Collectibles] = {}
+ h = maze._height
+ w = maze._width
+ for i in range(w):
+ for j in range(h):
+ if (i, j) == player.coordonate or maze.maze[j][i] == 15:
+ continue
+ if (i, j) in [(0, 0), (h - 1, 0), (0, w - 1), (h - 1, w - 1)]:
+ collectibles[(i, j)] = SuperPacgum(config)
+ else:
+ if random.randint(0, 100) < config.pacgum:
+ collectibles[(i, j)] = Pacgum(config)
+
+ self.collectibles = collectibles
+
+ def collect(self, pos: tuple[int, int]) -> tuple[int, bool]:
+ point = 0
+ hunt = False
+ if self.collectibles.get(pos):
+ pacgum = self.collectibles.pop(pos)
+ point = pacgum.collect()
+ if isinstance(pacgum, SuperPacgum):
+ hunt = True
+ return (point, hunt)
+
+ def is_empty(self) -> bool:
+ return len(self.collectibles) == 0
--- /dev/null
+from pydantic import ValidationError, BaseModel, Field
+from pathlib import Path
+from .error import error
+import json
+
+
+CELL_SIZE = 64
+
+
+class Configuration(BaseModel):
+ highscore_filename: str = Field(default="score.json")
+ levels: int = Field(ge=1, le=256, default=10)
+ width: int = Field(ge=5, le=20, default=13)
+ height: int = Field(ge=5, le=20, default=13)
+ lives: int = Field(ge=1, le=99, default=3)
+ pacgum: int = Field(ge=10, le=100, default=80)
+ points_per_pacgum: int = Field(ge=1, le=99, default=1)
+ points_per_super_pacgum: int = Field(ge=1, le=99, default=5)
+ points_per_ghost: int = Field(ge=1, le=99, default=20)
+ cheat_mod: bool = Field(default=False)
+ time: int = Field(ge=60, le=300, default=180)
+
+
+def load_config(path: str) -> Configuration:
+ if not path:
+ error("no file path given, default configuration applied.", False)
+ return Configuration()
+
+ file_path = Path(path)
+
+ if not file_path.exists():
+ error(f"file not found: '{path}', default configuration applied.",
+ False)
+ return Configuration()
+
+ text = ""
+ for item in file_path.read_text(encoding="utf-8").split("\n"):
+ if (
+ not item.strip().startswith("#") and
+ not item.strip().startswith("//")
+ ):
+ if text:
+ text += "\n"
+ text += item
+
+ try:
+ data = json.loads(text)
+ except json.JSONDecodeError:
+ error(f"invalid JSON in {path}, default configuration applied..",
+ False)
+ return Configuration()
+
+ try:
+ config = Configuration.model_validate(data)
+
+ except ValidationError as err:
+ errors = err.errors()
+
+ for e in errors:
+ field = e["loc"][0]
+
+ if e["type"] == "missing":
+ error(f"missing '{field}' configuration, default"
+ "value applied", False)
+ else:
+ error(f"wrong value for '{field}' configuration, "
+ "default value applied", False)
+
+ for e in errors:
+ data.pop(e["loc"][0], None)
+
+ config = Configuration.model_validate(data)
+
+ return config
--- /dev/null
+import sys
+
+
+def error(err: str, close: bool = True, line: int = 0) -> None:
+ """
+ Display an error message to the standard error stream and terminate
+ the program with exit code 1.
+
+ Args:
+ err (str): Description of the error.
+ line (int, optional): Line number where the error occurred.
+ If provided and greater than 0, it is included in the error
+ message. Defaults to 0.
+
+ Returns:
+ NoReturn: This function never returns because it terminates
+ the program.
+
+ Raises:
+ SystemExit: Always raised with exit code 1.
+ """
+ if line:
+ sys.stderr.write("\nError in line " + str(line) + ": " + err + "\n")
+ else:
+ sys.stderr.write("\nError: " + err + "\n")
+ sys.stderr.flush()
+ if close:
+ exit(1)
--- /dev/null
+from mazegenerator import MazeGenerator # type: ignore[import-untyped]
+from .config import Configuration
+from .score import ScoreManager
+from .render import MenuRenderer, GameRenderer, EndingRenderer
+from .render import Renderer, RenderInfo
+from .input import MenuInput, GameInput, EndingInput, Input
+from .player import Player
+from .collectibles import CollectableManager
+import pygame
+import time
+
+
+class Game:
+ def __init__(self, config: Configuration):
+ self.config = config
+ self.screen = pygame.display.set_mode((2560, 1440))
+ self.score_manager = ScoreManager(config.highscore_filename)
+ self.score = 0
+ self.lives = config.lives
+ self.maze = MazeGenerator(
+ (self.config.height, self.config.width),
+ False, (0, 0), (1, 1), 42
+ )
+ self.level = 0
+ self.render_info = self.get_render_info()
+ self.renderer: Renderer = MenuRenderer(
+ self.screen,
+ self.render_info,
+ self.score_manager)
+ self.input: Input = MenuInput(self.renderer, self.new_game, self.quit)
+ self.collectibles = CollectableManager()
+ self.player = Player(self.maze.maze)
+ self.running = True
+ self.start_time = 0.0
+ self.now_time = 0.0
+ self.run()
+
+ def add_collectibles(self) -> None:
+ self.collectibles.fill(self.config, self.maze, self.player)
+
+ def finish_game(self, victory: bool) -> None:
+ self.renderer = EndingRenderer(self.screen, self.render_info, victory)
+ self.input = EndingInput(self.renderer, self.add_score)
+
+ def win(self) -> None:
+ self.finish_game(True)
+
+ def loose(self) -> None:
+ self.finish_game(False)
+
+ def time_out(self) -> None:
+ if isinstance(self.renderer, GameRenderer):
+ self.renderer.time_out()
+ pygame.display.flip()
+ time.sleep(3)
+ self.loose()
+
+ def update_time(self) -> None:
+ self.now_time = time.time()
+ remain_time = self.config.time - (self.now_time - self.start_time)
+ self.render_info.remain_time = round(remain_time, 1)
+ if remain_time <= 0:
+ self.time_out()
+
+ def restart(self) -> None:
+ self.renderer = MenuRenderer(
+ self.screen,
+ self.render_info,
+ self.score_manager)
+ self.input = MenuInput(self.renderer, self.new_game, self.quit)
+
+ def get_render_info(self) -> RenderInfo:
+ return RenderInfo(
+ fps=0,
+ width=self.config.width,
+ height=self.config.height,
+ maze=self.maze.maze,
+ level=self.level,
+ score=self.score,
+ lives=self.lives,
+ remain_time=0.0
+ )
+
+ def next_level(self) -> None:
+ if self.level == 10:
+ self.win()
+ return
+ self.level += 1
+ self.maze.generate()
+ self.render_info.level = self.level
+ self.render_info.maze = self.maze.maze
+ self.player.next_level((1, 1), self.maze.maze)
+ if isinstance(self.renderer, GameRenderer):
+ self.renderer.next_level()
+ self.add_collectibles()
+ self.start_time = time.time()
+
+ def new_game(self) -> None:
+ self.score = 0
+ self.level = 0
+ self.lives = self.config.lives
+ self.renderer = GameRenderer(
+ self.screen,
+ self.render_info,
+ self.player,
+ self.collectibles)
+ self.input = GameInput(self.player, self.next_level, self.kill_player)
+ self.next_level()
+
+ def hunt(self) -> None:
+ print("Hunting ghost")
+ pass
+
+ def kill_player(self) -> None:
+ self.lives -= 1
+ self.render_info.lives = self.lives
+ if self.lives == 0:
+ self.loose()
+ else:
+ self.player.reset()
+
+ def pick_up(self) -> None:
+ point, hunt = self.collectibles.collect(self.player.coordonate)
+ self.score += point
+ self.render_info.score = self.score
+ if hunt:
+ self.hunt()
+ if self.collectibles.is_empty():
+ self.next_level()
+
+ def quit(self) -> None:
+ self.running = False
+
+ def run(self) -> None:
+ pygame.init()
+ pygame.font.init()
+ clock = pygame.time.Clock()
+
+ while self.running:
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ self.running = False
+ self.input.get_event(event)
+ keys = pygame.key.get_pressed()
+ self.input.get_key(keys)
+ self.render_info.fps = round(clock.get_fps(), 2)
+ if isinstance(self.input, GameInput):
+ self.player.move()
+ self.update_time()
+ if self.player.can_move():
+ self.pick_up()
+ self.screen.fill("black")
+ self.renderer.render()
+ pygame.display.flip()
+ clock.tick(60)
+ pygame.quit()
+
+ def add_score(self, name: str) -> None:
+ self.score_manager.add_score(name, self.score)
+ self.restart()
--- /dev/null
+from abc import ABC
+from collections.abc import Callable
+from .render import MenuRenderer, EndingRenderer
+from .player import Player
+import pygame
+import time
+
+
+class Input(ABC):
+ def get_event(self, event: pygame.event.Event) -> None:
+ return
+
+ def get_key(self, keys: pygame.key.ScancodeWrapper) -> None:
+ return
+
+
+class MenuInput(Input):
+ def __init__(self, renderer: MenuRenderer, new_game: Callable[[], None],
+ quit: Callable[[], None]) -> None:
+ self.renderer = renderer
+ self.index = 0
+ self.functions = {
+ 0: new_game,
+ 1: quit,
+ }
+ self.cooldown = 0.0
+
+ def change_index(self, value: int) -> None:
+ self.index = max(0, min(1, self.index + value))
+ self.renderer.index = self.index
+ self.cooldown = time.time()
+
+ def validation(self) -> None:
+ self.functions[self.index]()
+
+ def get_event(self, event: pygame.event.Event) -> None:
+ if event.type == pygame.KEYDOWN:
+ if event.key in [1073741906, 119]:
+ self.change_index(-1)
+ elif event.key in [1073741905, 115]:
+ self.change_index(1)
+ elif event.key in [13, 32]:
+ self.validation()
+
+
+class GameInput(Input):
+ def __init__(self, player: Player, next_lvl: Callable[[], None],
+ kill: Callable[[], None]):
+ self.player = player
+ self.next_level = next_lvl
+ self.kill = kill
+
+ def get_key(self, keys: pygame.key.ScancodeWrapper) -> None:
+ if keys[pygame.K_UP] or keys[pygame.K_w]:
+ self.player.go_to((0, -1))
+ elif keys[pygame.K_DOWN] or keys[pygame.K_s]:
+ self.player.go_to((0, 1))
+ elif keys[pygame.K_LEFT] or keys[pygame.K_a]:
+ self.player.go_to((-1, 0))
+ elif keys[pygame.K_RIGHT] or keys[pygame.K_d]:
+ self.player.go_to((1, 0))
+
+ def get_event(self, event: pygame.event.Event) -> None:
+ if event.type == pygame.KEYDOWN:
+ if event.unicode == "n":
+ self.next_level()
+ if event.unicode == "k":
+ self.kill()
+
+
+class EndingInput(Input):
+ def __init__(self, renderer: EndingRenderer,
+ add_score: Callable[[str], None]) -> None:
+ self.renderer = renderer
+ self.add_score = add_score
+ self.name = ""
+
+ def get_event(self, event: pygame.event.Event) -> None:
+ if event.type == pygame.TEXTINPUT:
+ c: str = event.text
+ if c.isalnum():
+ if len(self.name) < 10:
+ self.name += event.text
+ elif event.type == pygame.KEYDOWN:
+ if event.key == 8:
+ if self.name:
+ self.name = self.name[0:-1]
+ elif event.key == 32:
+ if len(self.name) < 10:
+ self.name += ' '
+ elif event.key == 13:
+ self.add_score(self.name)
+ self.renderer.name = self.name
--- /dev/null
+from .config import CELL_SIZE as CELL_SIZE
+import pygame
+
+
+class Player:
+ def __init__(self, maze: list[list[int]]):
+ self.position = (0, 0)
+ self.coordonate = (0, 0)
+ self.direction = (0, 0)
+ self.start_coordonate = (0, 0)
+ self.speed = 4
+ self.maze = maze
+ self.spritesheet = self.load_player_sprites()
+ self.sprite = self.spritesheet[(1, 0)]
+
+ def update_position(self) -> None:
+ self.position = (self.coordonate[0] * CELL_SIZE,
+ self.coordonate[1] * CELL_SIZE)
+
+ def next_level(self, pos: tuple[int, int], maze: list[list[int]]) -> None:
+ self.maze = maze
+ self.start_coordonate = pos
+ self.reset()
+
+ def reset(self) -> None:
+ self.sprite = self.spritesheet[(1, 0)]
+ self.direction = (0, 0)
+ self.coordonate = self.start_coordonate
+ self.update_position()
+
+ @staticmethod
+ def load_player_sprites() -> dict[tuple[int, int], pygame.Surface]:
+ player_textures = {}
+ for t, l in [((0, 1), "D"),
+ ((0, -1), "U"),
+ ((1, 0), "R"),
+ ((-1, 0), "L")]:
+ filename = "asset/player/player_" + str(l) + "A.png"
+ img = pygame.image.load(filename)
+ player_textures[t] = img
+ return player_textures
+
+ def get_sprite(self) -> pygame.Surface:
+ if self.direction != (0, 0):
+ self.sprite = self.spritesheet[self.direction]
+ return self.sprite
+
+ @staticmethod
+ def binary(cell: int) -> list[int]:
+ b = bin(cell)[2:].zfill(4)
+ r = [1 if i == "1" else 0 for i in b]
+ return r[::-1]
+
+ def can_go(self, dir: tuple[int, int]) -> bool:
+ cell = self.maze[self.coordonate[1]][self.coordonate[0]]
+ index = [(0, -1), (1, 0), (0, 1), (-1, 0)].index(dir)
+ return self.binary(cell)[index] == 0
+
+ def can_move(self) -> bool:
+ x = self.position[0]
+ y = self.position[1]
+ return (x % CELL_SIZE == 0 and y % CELL_SIZE == 0)
+
+ def go_to(self, dir: tuple[int, int]) -> None:
+ if not self.can_go(dir) or not self.can_move():
+ return
+ self.direction = dir
+
+ def move(self) -> None:
+ if self.direction == (0, 0):
+ return
+ x = self.position[0] + self.direction[0] * self.speed
+ y = self.position[1] + self.direction[1] * self.speed
+ self.position = (x, y)
+ if self.can_move():
+ x = self.coordonate[0] + self.direction[0]
+ y = self.coordonate[1] + self.direction[1]
+ self.coordonate = (x, y)
+ if not self.can_go(self.direction):
+ self.direction = (0, 0)
--- /dev/null
+from pydantic import BaseModel
+from abc import ABC, abstractmethod
+from .config import CELL_SIZE as CELL_SIZE
+from .score import ScoreManager
+from .player import Player
+from .collectibles import CollectableManager
+import pygame
+import time
+
+
+class RenderInfo(BaseModel):
+ fps: float
+ width: int
+ height: int
+ maze: list[list[int]]
+ level: int
+ score: int
+ lives: int
+ remain_time: float
+
+
+class Renderer(ABC):
+ def __init__(self, screen: pygame.Surface, info: RenderInfo) -> None:
+ self.screen = screen
+ self.info = info
+ super().__init__()
+
+ @abstractmethod
+ def render(self) -> None:
+ ...
+
+ def draw_text(self, text: str, font_color: tuple[int, int, int],
+ font_size: int, position: tuple[int, int]) -> int:
+ font = pygame.font.Font(None, font_size)
+ txt = font.render(text, True, font_color)
+ self.screen.blit(txt, position)
+ return txt.get_width()
+
+
+class Animate_chararacter:
+ def __init__(self, sprite: str):
+ self.sprite = {letter: pygame.image.load("asset/enemy/ghost_blue.png")
+ if ("ghost" in sprite and letter == "R")
+ else pygame.image.load(sprite.replace("*", letter))
+ for letter in ["L", "R"]}
+
+ def get_sprite(self, dir: str) -> pygame.Surface:
+ return self.sprite[dir]
+
+
+class MenuRenderer(Renderer):
+ def __init__(self, screen: pygame.Surface, info: RenderInfo,
+ score_manager: ScoreManager) -> None:
+ super().__init__(screen, info)
+ self.index = 0
+ self.characters = self.add_animated_characters()
+ self.pos = (-1000, 100)
+ self.dir = "R"
+ self.score_manager = score_manager
+
+ def add_animated_characters(self) -> list[Animate_chararacter]:
+ char = [Animate_chararacter("asset/player/player_*A.png")]
+ for letter in "ABCD":
+ s = "asset/enemy/ghost" + letter + "_*.png"
+ char.append(Animate_chararacter(s))
+ return char
+
+ def move(self) -> None:
+ self.pos = (self.pos[0] + 8 * (1 if self.dir == "R" else -1),
+ self.pos[1])
+
+ if self.pos[0] > 2700:
+ self.dir = "L"
+ if self.pos[0] < -600:
+ self.dir = "R"
+
+ def draw_character(self) -> None:
+ self.move()
+ padding = 80
+ i = 0
+ for c in self.characters:
+ pos = (self.pos[0] + i * padding + (100 if i else 0), self.pos[1])
+ self.screen.blit(c.get_sprite(self.dir), pos)
+ i += 1
+
+ def highscore(self) -> None:
+ x = 1800
+ y = 400
+ padding = 50
+
+ self.draw_text("High Score", (255, 255, 255), 100, (x + 50, y))
+
+ count = 0
+ lst = sorted(self.score_manager.score_list,
+ key=lambda x: x.score, reverse=True)
+ for s in lst:
+ count += 1
+ txt = str(count) + ". " + s.name + " : " + str(s.score)
+ self.draw_text(txt, (255, 255, 255), 40,
+ (x + 100, y + 100 + count * padding))
+ if count == 10:
+ break
+
+ def instructions(self) -> None:
+ x = 100
+ y = 400
+ padding = 70
+ size = 40
+ self.draw_text("Instructions", (255, 255, 255), 100, (x + 50, y))
+ self.draw_text("Use arrows or WASD to move in the maze.",
+ (255, 255, 255), size, (x, y + 2 * padding))
+ self.draw_text("Pick up every pacgum to win the level.",
+ (255, 255, 255), size, (x, y + 3 * padding))
+ self.draw_text("If a ghost touch you, you lose a life.",
+ (255, 255, 255), size, (x, y + 4 * padding))
+ self.draw_text("If your life or the timer go to 0 you lose.",
+ (255, 255, 255), size, (x, y + 5 * padding))
+ self.draw_text("If you finish every level you win.",
+ (255, 255, 255), size, (x, y + 6 * padding))
+
+ def render(self,) -> None:
+ self.instructions()
+ self.highscore()
+ self.draw_character()
+ x = 1130
+ y = 600
+ padding = 150
+ self.draw_text("PAC-MAN", (255, 255, 0), 200, (x - 180, 200))
+ len = self.draw_text("New Game", (255, 255, 255), 80, (x, y))
+ self.draw_text("Quit", (255, 255, 255), 80, (x + 80, y + padding))
+ rect = pygame.Rect(x - 20, y - 20 + self.index * padding, len + 40, 92)
+ pygame.draw.rect(self.screen, (255, 255, 255), rect, 4)
+
+
+class GameRenderer(Renderer):
+ def __init__(self, screen: pygame.Surface, info: RenderInfo,
+ player: Player, collectibles: CollectableManager):
+ super().__init__(screen, info)
+ self.maze = self.render_maze()
+ self.player = player
+ self.collectibles = collectibles
+ self.x = 500
+ self.y = 80 + (1280 - self.info.height * CELL_SIZE) / 2
+
+ @staticmethod
+ def load_wall_textures() -> dict[int, pygame.Surface]:
+ return {n: pygame.image.load("asset/walls/" + str(n) + ".png")
+ for n in range(16)}
+
+ def render_maze(self) -> pygame.Surface:
+ maze_surface = pygame.Surface(
+ (self.info.width * CELL_SIZE, self.info.height * CELL_SIZE))
+ walls_textures = self.load_wall_textures()
+ y = 0
+ for row in self.info.maze:
+ x = 0
+ for cell in row:
+ img = walls_textures[cell]
+ maze_surface.blit(img, (x * CELL_SIZE, y * CELL_SIZE))
+ x += 1
+ y += 1
+ return maze_surface
+
+ def next_level(self) -> None:
+ self.maze = self.render_maze()
+
+ def draw_HUD(self) -> None:
+ x = 200
+ y = 100
+ padding = 40
+ self.draw_text("FPS: " + str(self.info.fps), (255, 255, 255),
+ 30, (x, y))
+ self.draw_text("Level: " + str(self.info.level), (255, 255, 255),
+ 30, (x, y + padding))
+ self.draw_text("Lives: " + str(self.info.lives), (255, 255, 255),
+ 30, (x, y + 2 * padding))
+ self.draw_text("Score: " + str(self.info.score), (255, 255, 255),
+ 30, (x, y + 3 * padding))
+ self.draw_text("Time: " + str(self.info.remain_time), (255, 255, 255),
+ 30, (x, y + 4 * padding))
+
+ def render(self) -> None:
+ self.draw_HUD()
+ self.screen.blit(self.maze, (self.x, self.y))
+ player_sprite = self.player.get_sprite()
+ x = self.x + self.player.position[0]
+ y = self.y + self.player.position[1]
+ self.screen.blit(player_sprite, (x, y))
+ for i in self.collectibles.collectibles:
+ pacgum = self.collectibles.collectibles[i]
+ size = pacgum.size
+ center = (
+ self.x + i[0] * CELL_SIZE + CELL_SIZE // 2,
+ self.y + i[1] * CELL_SIZE + CELL_SIZE // 2
+ )
+ pygame.draw.circle(self.screen, (255, 255, 255), center, size)
+
+ def time_out(self) -> None:
+ x = self.x + (self.info.width * CELL_SIZE) / 2 - 200
+ y = self.y + (self.info.height * CELL_SIZE) / 2 - 50
+ rect = pygame.Rect(x - 22, y - 21, 372, 110)
+ pygame.draw.rect(self.screen, (0, 0, 0), rect)
+ pygame.draw.rect(self.screen, (255, 255, 255), rect, 4)
+
+
+class EndingRenderer(Renderer):
+ def __init__(self, screen: pygame.Surface, info: RenderInfo,
+ victory: bool) -> None:
+ super().__init__(screen, info)
+ self.name = ""
+ self.victory = victory
+
+ def render(self) -> None:
+ x = 900
+ y = 500
+ if self.victory:
+ self.draw_text("YOU WIN !", (255, 255, 50), 200, (x + 50, y - 100))
+ self.draw_text("Enter your name", (255, 255, 255), 50,
+ (x + 250, y + 300))
+ else:
+ self.draw_text("GAME OVER", (200, 50, 50), 200, (x, y - 100))
+ self.draw_text("Enter your name, loser", (255, 255, 255), 50,
+ (x + 200, y + 300))
+ self.draw_text("Your score : " + str(self.info.score), (255, 255, 255),
+ 80, (x + 200, y + 200))
+ txt = self.draw_text(self.name, (255, 255, 255), 50,
+ (x + 400 - 10 * len(self.name), y + 400))
+ if round(time.time()) % 2:
+ rect = pygame.Rect(x + 405 + txt - 10 * len(self.name),
+ y + 395, 4, 40)
+ pygame.draw.rect(self.screen, (255, 255, 255), rect)
--- /dev/null
+from pydantic import ValidationError, BaseModel
+from pathlib import Path
+from .error import error
+import json
+
+
+class ScoreDefinition(BaseModel):
+ name: str
+ score: int
+
+
+class ScoreManager:
+ def __init__(self, path: str) -> None:
+ file_path = Path(path)
+ self.score_list: list[ScoreDefinition] = []
+ self.file_path = file_path
+ if not file_path.exists():
+ error("No highscore file found : new file created", False)
+ self.save()
+ else:
+ self.load()
+
+ def score_error(self) -> None:
+ error("Highscore file corrupted : new file created", False)
+ self.save()
+
+ def load(self) -> None:
+ try:
+ data = json.loads(self.file_path.read_text(encoding="utf-8"))
+ except json.JSONDecodeError:
+ return self.score_error()
+
+ if not isinstance(data, list):
+ return self.score_error()
+
+ score_list: list[ScoreDefinition] = []
+
+ for item in data:
+ try:
+ score_list.append(ScoreDefinition.model_validate(item))
+ except ValidationError:
+ return self.score_error()
+ self.score_list = score_list
+
+ def save(self) -> None:
+ data = [item.model_dump() for item in self.score_list]
+ self.file_path.parent.mkdir(parents=True, exist_ok=True)
+ self.file_path.write_text(
+ json.dumps(data, indent=4, ensure_ascii=False),
+ encoding="utf-8"
+ )
+
+ def add_score(self, name: str, score: int) -> None:
+ try:
+ item = ScoreDefinition(
+ name=name,
+ score=score
+ )
+ except ValidationError:
+ return
+ exist = False
+ for i in self.score_list:
+ if i.name == name:
+ exist = True
+ if i.score < score:
+ i.score = score
+ if not exist:
+ self.score_list.append(item)
+ self.save()