+++ /dev/null
-from pacman import main
-
-if __name__ == "__main__":
- main()
+++ /dev/null
-import sys
-
-from pacman.config import load_config
-from pacman.error import error
-from pacman.game import Game
-
-
-def main() -> None:
- try:
- file_path = sys.argv[1]
- except IndexError:
- file_path = ""
- config = load_config(file_path)
- try:
- Game(config)
- except KeyboardInterrupt:
- error("keyboardInterrupt.")
+++ /dev/null
-import random
-from abc import ABC
-
-from mazegenerator import MazeGenerator
-
-from pacman.config import Configuration
-from pacman.player import Player
-
-
-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
-import json
-from pathlib import Path
-
-from pydantic import BaseModel, Field, ValidationError
-
-from pacman.error import error
-
-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, defaultvalue 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
-import time
-
-import pygame
-from mazegenerator import MazeGenerator
-
-from .collectibles import CollectableManager
-from .config import Configuration
-from .input import EndingInput, GameInput, Input, MenuInput
-from .player import Player
-from .render import (
- EndingRenderer,
- GameRenderer,
- MenuRenderer,
- Renderer,
- RenderInfo,
-)
-from .score import ScoreManager
-
-
-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
-import time
-from abc import ABC
-from collections.abc import Callable
-from typing import override
-
-import pygame
-
-from pacman.player import Player
-from pacman.render import EndingRenderer, MenuRenderer
-
-
-class Input(ABC):
- def get_event(self, event: pygame.event.Event) -> None:
- return None
-
- def get_key(self, keys: pygame.key.ScancodeWrapper) -> None:
- return None
-
-
-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]()
-
- @override
- 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
- self.next_move: tuple[int, int] | None = None
-
- @override
- def get_key(self, keys: pygame.key.ScancodeWrapper) -> None:
- if keys[pygame.K_UP] or keys[pygame.K_w]:
- self.next_move = (0, -1)
- elif keys[pygame.K_DOWN] or keys[pygame.K_s]:
- self.next_move = (0, 1)
- elif keys[pygame.K_LEFT] or keys[pygame.K_a]:
- self.next_move = (-1, 0)
- elif keys[pygame.K_RIGHT] or keys[pygame.K_d]:
- self.next_move = (1, 0)
- if self.player.can_move() and self.next_move:
- self.player.go_to(self.next_move)
- self.next_move = None
-
- @override
- 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 = ""
-
- @override
- 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
-import pygame
-
-from pacman.config import CELL_SIZE as CELL_SIZE
-
-
-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
-import time
-from abc import ABC, abstractmethod
-
-import pygame
-from pydantic import BaseModel
-
-from pacman.collectibles import CollectableManager
-from pacman.config import CELL_SIZE as CELL_SIZE
-from pacman.player import Player
-from pacman.score import ScoreManager
-
-
-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
-import json
-from pathlib import Path
-
-from pydantic import BaseModel, ValidationError
-
-from pacman.error import error
-
-
-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()
self._components_lookup: dict[type, ComponentID] = {}
self._schedules: dict[ScheduleLabel, Schedule[RawSystem[Any]]] = {}
self._base_archetype: Archetype = Archetype(self, ())
+ self._extractors_cache: dict[
+ System[Any], tuple[Callable[[], FromWorld | None], ...]
+ ] = {}
def component_id(self, comp: type) -> ComponentID:
if (res := self._components_lookup.get(comp)) is None:
def _sys_extractors[T](
self, system: System[T]
) -> tuple[Callable[[], FromWorld | None], ...]:
+ if system in self._extractors_cache:
+ return self._extractors_cache[system]
sig = inspect.signature(system, eval_str=True)
if err := next(
filter(
None,
):
raise TypeError(f"System {system}'s {err[0]} parameter is invalid")
- return tuple(
+ res = tuple(
cast(type[FromWorld], param.annotation).extract_from_world(
param.annotation, self
)
for param in sig.parameters.values()
)
+ self._extractors_cache[system] = res
+ return res
def add_system[T, *P](
self,
from enum import Enum, auto
from random import random, shuffle
from time import time
+from typing import override
from mazegenerator import MazeGenerator # type: ignore[import-untyped]
from pygame import K_a, K_d, K_s, K_w, Vector2
@dataclass
class Pacman(Component):
- pass
+ home: tuple[int, int]
+ home_entity: Entity
class GhostState(Enum):
def setup_maze(world: World) -> None:
- world.add_system(create_maze)(MazeGenerator((5, 5)))
+ world.add_system(create_maze)(MazeGenerator((14, 14)))
def move_in_motion(
def spawn_pacman(
- world: World, assets: Assets[Sprite2D], maze_q: Query[tuple[Maze, Entity]]
+ world: World,
+ assets: Assets[Sprite2D],
+ maze_q: Query[tuple[Maze, Entity]],
+ cells: Query[Cell],
) -> None:
if (maze_t := maze_q.single()) is None:
return
maze, maze_ent = maze_t
+ home_cell = sorted(
+ (cell for cell in cells if cell.n != 15),
+ key=lambda cell: Vector2(cell.grid_pos).distance_squared_to(
+ (Vector2(maze._x, maze._y).elementwise() / 2).elementwise() - 0.5
+ ),
+ )[0]
+ pos = home_cell.grid_pos
world.spawn(
tuple[
Coord2D,
ChildOf,
],
(
- Coord2D(3 * 64, 3 * 64, -10),
+ Coord2D(pos[0] * 64, pos[1] * 64, -10),
CoordAbs2D(),
AnimationSet(
[
],
6,
),
- Pacman(),
+ Pacman(pos, maze._cells[pos]),
ShouldRender(),
- InMotion(maze._cells[(3, 3)], maze._cells[(3, 3)], 1),
+ InMotion(maze._cells[(pos)], maze._cells[(pos)], 1),
MoveSpeed(3.0),
- TilePos(maze._cells[(3, 3)]),
+ TilePos(maze._cells[(pos)]),
ChildOf(maze_ent),
),
)
world.despawn_many(collided)
+def respawn_pacman(pacmans: Query[tuple[Pacman, Coord2D, InMotion]]) -> None:
+ for pacman, pos, motion in pacmans:
+ pos.xy = Vector2(*pacman.home).elementwise() * 64
+ motion.pos = pacman.home_entity
+ motion.src = pacman.home_entity
+
+
+def respawn_ghost(ghosts: Query[tuple[Ghost, Coord2D, InMotion]]) -> None:
+ for ghost, pos, motion in ghosts:
+ pos.xy = Vector2(*ghost.home).elementwise() * 64
+ motion.pos = ghost.home_entity
+ motion.src = ghost.home_entity
+
+
def collide_ghost(
pacmans: Query[CoordAbs2D, Pacman],
ghosts: Query[
Ghost,
],
score: Score,
+ world: World,
) -> None:
for pacman_pos in pacmans:
for ghost_pos, status, ghost, motion, pos in ghosts:
continue
match status.state:
case GhostState.wander | GhostState.chase:
- # TODO: kill pacman
- print("Pacman hit")
+ world.add_raw_system(respawn_pacman)()
+ world.add_raw_system(respawn_ghost)()
+ world.add_raw_system(start_pause_timer)()
case GhostState.flee:
score.score += 100 # FIXME
world.spawn_resource(Score())
+def start_pause_timer(timer: "InitialPauseTimer") -> None:
+ timer.tick = 0
+
+
+def setup_new_maze(world: World) -> None:
+ for sys in (
+ setup_maze,
+ spawn_superpacgums,
+ spawn_pacgums,
+ spawn_pacman,
+ spawn_ghosts,
+ start_pause_timer,
+ ):
+ world.add_raw_system(sys)()
+
+
def update_inputs(inputs: Inputs, events: WindowEvents) -> None:
for key, state in events.keys_itr():
if state:
) -> None:
for maze in mazes:
queue.queue(hierarchy.despawn_with_children(maze))
+ queue.queue(setup_new_maze)
+
+
+@dataclass
+class InitialPauseTimer(Resource):
+ tick: int = 0
+
+ @classmethod
+ @override
+ def extract_default(
+ cls, ty: type, world: "World"
+ ) -> Callable[[], "InitialPauseTimer | None"]:
+ return InitialPauseTimer
+
+
+def initial_pause_done(timer: InitialPauseTimer) -> bool:
+ if timer.tick < 60:
+ timer.tick += 1
+ return False
+ return True
def run_game() -> None:
World().with_plugins(graphics_plugins, minimal_plugins).with_systems(
StartupSchedule,
- Systems(
- setup_resources,
- setup_maze,
- spawn_superpacgums,
- spawn_pacgums,
- spawn_pacman,
- spawn_ghosts,
- ).chain(),
+ Systems(setup_resources, setup_new_maze).chain(),
).with_systems(
UpdateSchedule,
Systems(
update_ghost_speed,
)
.chain()
+ .run_if(initial_pause_done)
.in_set(Update),
Systems(update_inputs).in_set(PreUpdate).after(InputsSystems),
Systems(update_pacman_anim, update_ghost_anim)
collect_superpacgum,
Systems(collide_ghost).before(update_ghost_anim),
)
+ .chain()
.after(PropagateTransform)
.before(GraphicsSystems),
Systems(victory).run_if(level_is_finished),