From: Thomas Rimelen Date: Wed, 26 Aug 2026 15:53:42 +0000 (+0200) Subject: pacman v1 X-Git-Url: https://git.uwuaxy.net/sitemap.xml?a=commitdiff_plain;h=86489aca2261c1d74ebb1d1cb094189bda94f9cc;p=axy%2Fft%2Fpacman.git pacman v1 --- 86489aca2261c1d74ebb1d1cb094189bda94f9cc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..734025d --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +**/add.mypy_cache +orga.txt +Project Management.xlsx +cell.txt +.venv +mazegeneratordemerde.txt +a.txt +**/__pycache__ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..f541daa --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +*This project has been created as part of the 42 curriculum by agilliar and trimelen* diff --git a/asset/enemy/ghostA_D.png b/asset/enemy/ghostA_D.png new file mode 100644 index 0000000..177aaf4 Binary files /dev/null and b/asset/enemy/ghostA_D.png differ diff --git a/asset/enemy/ghostA_L.png b/asset/enemy/ghostA_L.png new file mode 100644 index 0000000..919077c Binary files /dev/null and b/asset/enemy/ghostA_L.png differ diff --git a/asset/enemy/ghostA_R.png b/asset/enemy/ghostA_R.png new file mode 100644 index 0000000..609f29c Binary files /dev/null and b/asset/enemy/ghostA_R.png differ diff --git a/asset/enemy/ghostA_U.png b/asset/enemy/ghostA_U.png new file mode 100644 index 0000000..e4febc5 Binary files /dev/null and b/asset/enemy/ghostA_U.png differ diff --git a/asset/enemy/ghostB_D.png b/asset/enemy/ghostB_D.png new file mode 100644 index 0000000..bef0bf6 Binary files /dev/null and b/asset/enemy/ghostB_D.png differ diff --git a/asset/enemy/ghostB_L.png b/asset/enemy/ghostB_L.png new file mode 100644 index 0000000..b1534d4 Binary files /dev/null and b/asset/enemy/ghostB_L.png differ diff --git a/asset/enemy/ghostB_R.png b/asset/enemy/ghostB_R.png new file mode 100644 index 0000000..4844ad3 Binary files /dev/null and b/asset/enemy/ghostB_R.png differ diff --git a/asset/enemy/ghostB_U.png b/asset/enemy/ghostB_U.png new file mode 100644 index 0000000..a260f74 Binary files /dev/null and b/asset/enemy/ghostB_U.png differ diff --git a/asset/enemy/ghostC_D.png b/asset/enemy/ghostC_D.png new file mode 100644 index 0000000..0e8839c Binary files /dev/null and b/asset/enemy/ghostC_D.png differ diff --git a/asset/enemy/ghostC_L.png b/asset/enemy/ghostC_L.png new file mode 100644 index 0000000..bdd3e17 Binary files /dev/null and b/asset/enemy/ghostC_L.png differ diff --git a/asset/enemy/ghostC_R.png b/asset/enemy/ghostC_R.png new file mode 100644 index 0000000..f4c823d Binary files /dev/null and b/asset/enemy/ghostC_R.png differ diff --git a/asset/enemy/ghostC_U.png b/asset/enemy/ghostC_U.png new file mode 100644 index 0000000..e05a7ed Binary files /dev/null and b/asset/enemy/ghostC_U.png differ diff --git a/asset/enemy/ghostD_D.png b/asset/enemy/ghostD_D.png new file mode 100644 index 0000000..0d0685e Binary files /dev/null and b/asset/enemy/ghostD_D.png differ diff --git a/asset/enemy/ghostD_L.png b/asset/enemy/ghostD_L.png new file mode 100644 index 0000000..0e4fe14 Binary files /dev/null and b/asset/enemy/ghostD_L.png differ diff --git a/asset/enemy/ghostD_R.png b/asset/enemy/ghostD_R.png new file mode 100644 index 0000000..706f761 Binary files /dev/null and b/asset/enemy/ghostD_R.png differ diff --git a/asset/enemy/ghostD_U.png b/asset/enemy/ghostD_U.png new file mode 100644 index 0000000..1b4b625 Binary files /dev/null and b/asset/enemy/ghostD_U.png differ diff --git a/asset/enemy/ghost_blue.png b/asset/enemy/ghost_blue.png new file mode 100644 index 0000000..760b2de Binary files /dev/null and b/asset/enemy/ghost_blue.png differ diff --git a/asset/player/player_DA.png b/asset/player/player_DA.png new file mode 100644 index 0000000..50c265a Binary files /dev/null and b/asset/player/player_DA.png differ diff --git a/asset/player/player_DB.png b/asset/player/player_DB.png new file mode 100644 index 0000000..71ab862 Binary files /dev/null and b/asset/player/player_DB.png differ diff --git a/asset/player/player_DC.png b/asset/player/player_DC.png new file mode 100644 index 0000000..d65c866 Binary files /dev/null and b/asset/player/player_DC.png differ diff --git a/asset/player/player_LA.png b/asset/player/player_LA.png new file mode 100644 index 0000000..d3e5fe9 Binary files /dev/null and b/asset/player/player_LA.png differ diff --git a/asset/player/player_LB.png b/asset/player/player_LB.png new file mode 100644 index 0000000..9c6cb7a Binary files /dev/null and b/asset/player/player_LB.png differ diff --git a/asset/player/player_LC.png b/asset/player/player_LC.png new file mode 100644 index 0000000..5ab5be9 Binary files /dev/null and b/asset/player/player_LC.png differ diff --git a/asset/player/player_RA.png b/asset/player/player_RA.png new file mode 100644 index 0000000..641500d Binary files /dev/null and b/asset/player/player_RA.png differ diff --git a/asset/player/player_RB.png b/asset/player/player_RB.png new file mode 100644 index 0000000..1537710 Binary files /dev/null and b/asset/player/player_RB.png differ diff --git a/asset/player/player_RC.png b/asset/player/player_RC.png new file mode 100644 index 0000000..5d1d33a Binary files /dev/null and b/asset/player/player_RC.png differ diff --git a/asset/player/player_UA.png b/asset/player/player_UA.png new file mode 100644 index 0000000..0f43f68 Binary files /dev/null and b/asset/player/player_UA.png differ diff --git a/asset/player/player_UB.png b/asset/player/player_UB.png new file mode 100644 index 0000000..af99964 Binary files /dev/null and b/asset/player/player_UB.png differ diff --git a/asset/player/player_UC.png b/asset/player/player_UC.png new file mode 100644 index 0000000..9f6b4d1 Binary files /dev/null and b/asset/player/player_UC.png differ diff --git a/asset/walls/0.png b/asset/walls/0.png new file mode 100644 index 0000000..7479afa Binary files /dev/null and b/asset/walls/0.png differ diff --git a/asset/walls/1.png b/asset/walls/1.png new file mode 100644 index 0000000..8591da0 Binary files /dev/null and b/asset/walls/1.png differ diff --git a/asset/walls/10.png b/asset/walls/10.png new file mode 100644 index 0000000..1efcfbd Binary files /dev/null and b/asset/walls/10.png differ diff --git a/asset/walls/11.png b/asset/walls/11.png new file mode 100644 index 0000000..c2f3e8e Binary files /dev/null and b/asset/walls/11.png differ diff --git a/asset/walls/12.png b/asset/walls/12.png new file mode 100644 index 0000000..5cb94d8 Binary files /dev/null and b/asset/walls/12.png differ diff --git a/asset/walls/13.png b/asset/walls/13.png new file mode 100644 index 0000000..2b18403 Binary files /dev/null and b/asset/walls/13.png differ diff --git a/asset/walls/14.png b/asset/walls/14.png new file mode 100644 index 0000000..c4b299e Binary files /dev/null and b/asset/walls/14.png differ diff --git a/asset/walls/15.png b/asset/walls/15.png new file mode 100644 index 0000000..b01342b Binary files /dev/null and b/asset/walls/15.png differ diff --git a/asset/walls/2.png b/asset/walls/2.png new file mode 100644 index 0000000..61318a7 Binary files /dev/null and b/asset/walls/2.png differ diff --git a/asset/walls/3.png b/asset/walls/3.png new file mode 100644 index 0000000..3b26f8a Binary files /dev/null and b/asset/walls/3.png differ diff --git a/asset/walls/4.png b/asset/walls/4.png new file mode 100644 index 0000000..66f49a1 Binary files /dev/null and b/asset/walls/4.png differ diff --git a/asset/walls/5.png b/asset/walls/5.png new file mode 100644 index 0000000..63b3804 Binary files /dev/null and b/asset/walls/5.png differ diff --git a/asset/walls/6.png b/asset/walls/6.png new file mode 100644 index 0000000..8f1faf5 Binary files /dev/null and b/asset/walls/6.png differ diff --git a/asset/walls/7.png b/asset/walls/7.png new file mode 100644 index 0000000..36c05c2 Binary files /dev/null and b/asset/walls/7.png differ diff --git a/asset/walls/8.png b/asset/walls/8.png new file mode 100644 index 0000000..7632e52 Binary files /dev/null and b/asset/walls/8.png differ diff --git a/asset/walls/9.png b/asset/walls/9.png new file mode 100644 index 0000000..adc0bc8 Binary files /dev/null and b/asset/walls/9.png differ diff --git a/config.json b/config.json new file mode 100644 index 0000000..39bbc53 --- /dev/null +++ b/config.json @@ -0,0 +1,13 @@ +{ + "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 diff --git a/mazegenerator-00001-py3-none-any.whl b/mazegenerator-00001-py3-none-any.whl new file mode 100644 index 0000000..03d6ac1 Binary files /dev/null and b/mazegenerator-00001-py3-none-any.whl differ diff --git a/score.json b/score.json new file mode 100644 index 0000000..02d2466 --- /dev/null +++ b/score.json @@ -0,0 +1,26 @@ +[ + { + "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 diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/__main__.py b/src/__main__.py new file mode 100644 index 0000000..d2af9a8 --- /dev/null +++ b/src/__main__.py @@ -0,0 +1,22 @@ +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.") diff --git a/src/collectibles.py b/src/collectibles.py new file mode 100644 index 0000000..c5e6d8e --- /dev/null +++ b/src/collectibles.py @@ -0,0 +1,61 @@ +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 diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..b45b7d4 --- /dev/null +++ b/src/config.py @@ -0,0 +1,74 @@ +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 diff --git a/src/error.py b/src/error.py new file mode 100644 index 0000000..b597bae --- /dev/null +++ b/src/error.py @@ -0,0 +1,28 @@ +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) diff --git a/src/game.py b/src/game.py new file mode 100644 index 0000000..6fe1004 --- /dev/null +++ b/src/game.py @@ -0,0 +1,160 @@ +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() diff --git a/src/input.py b/src/input.py new file mode 100644 index 0000000..91d325a --- /dev/null +++ b/src/input.py @@ -0,0 +1,93 @@ +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 diff --git a/src/player.py b/src/player.py new file mode 100644 index 0000000..7427558 --- /dev/null +++ b/src/player.py @@ -0,0 +1,80 @@ +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) diff --git a/src/render.py b/src/render.py new file mode 100644 index 0000000..6a9c386 --- /dev/null +++ b/src/render.py @@ -0,0 +1,231 @@ +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) diff --git a/src/score.py b/src/score.py new file mode 100644 index 0000000..39b8a12 --- /dev/null +++ b/src/score.py @@ -0,0 +1,69 @@ +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()