diff --git a/actors/enums.py b/actors/enums.py index 45f4e39..4b86c41 100644 --- a/actors/enums.py +++ b/actors/enums.py @@ -1,4 +1,4 @@ -from enum import Enum +from enum import Enum, auto class PlayerDirection(Enum): DirectionRight = 0 @@ -21,4 +21,30 @@ class Colors(Enum): Black = (0, 0, 0) Blue = (0, 0, 255) Cyan = (0, 255, 255) - Magenta = (255, 0, 255) \ No newline at end of file + Magenta = (255, 0, 255) + +class GhostColor(Enum): + BLINKY = (255, 0, 0) + PINKY = (255, 184, 255) + INKY = (0, 255, 255) + CLYDE = (255, 184, 82) + +class GhostMode(Enum): + SCATTER = auto() + CHASE = auto() + FRIGHTENED = auto() + +class GhostBehavior(Enum): + BLINKY = "blinky_behavior" + PINKY = "pinky_behavior" + INKY = "inky_behavior" + CLYDE = "clyde_behavior" + + def decide_direction(self, ghost, pacman, maze): + strategy = { + GhostBehavior.BLINKY: blinky_behavior, + GhostBehavior.PINKY: pinky_behavior, + GhostBehavior.INKY: inky_behavior, + GhostBehavior.CLYDE: clyde_behavior, + }[self] + return strategy(ghost, pacman, maze) \ No newline at end of file diff --git a/actors/ghost.py b/actors/ghost.py index 7775768..3c45261 100644 --- a/actors/ghost.py +++ b/actors/ghost.py @@ -1,5 +1,59 @@ -from .enums import GhostDirection +from enums import GhostColor, GhostMode, GhostBehavior +from behaviors import path_toward # required if you want a fallback -class ActorGhost: - def __init__(self): - pass \ No newline at end of file +class Ghost(pygame.sprite.Sprite): + def __init__(self, name, color_enum, behavior_enum, position, speed): + super().__init__() + self.name = name + self.color = color_enum.value + self.behavior = behavior_enum + self.image = pygame.Surface((16, 16)) + self.image.fill(self.color) + self.rect = self.image.get_rect(center=position) + self.speed = speed + self.direction = pygame.Vector2(1, 0) + self.mode = GhostMode.SCATTER + self.home_position = position + + def update(self, maze, pacman): + if self.mode == GhostMode.FRIGHTENED: + self.change_direction_randomly(maze) + else: + self.direction = self.behavior.decide_direction(self, pacman, maze) + new_pos = self.rect.move(self.direction.x * self.speed, self.direction.y * self.speed) + if not maze.is_wall(new_pos.center): + self.rect = new_pos + + def change_direction_randomly(self, maze): + import random + directions = [pygame.Vector2(1, 0), pygame.Vector2(-1, 0), + pygame.Vector2(0, 1), pygame.Vector2(0, -1)] + random.shuffle(directions) + for d in directions: + test_pos = self.rect.move(d.x * self.speed, d.y * self.speed) + if not maze.is_wall(test_pos.center): + self.direction = d + break + + def set_mode(self, mode: GhostMode): + self.mode = mode + if mode == GhostMode.FRIGHTENED: + self.image.fill((33, 33, 255)) # dark blue + else: + self.image.fill(self.color) + +class Blinky(Ghost): + def __init__(self, position): + super().__init__("Blinky", GhostColor.BLINKY, GhostBehavior.BLINKY, position, speed=2) + +class Pinky(Ghost): + def __init__(self, position): + super().__init__("Pinky", GhostColor.PINKY, GhostBehavior.PINKY, position, speed=2) + +class Inky(Ghost): + def __init__(self, position): + super().__init__("Inky", GhostColor.INKY, GhostBehavior.INKY, position, speed=2) + +class Clyde(Ghost): + def __init__(self, position): + super().__init__("Clyde", GhostColor.CLYDE, GhostBehavior.CLYDE, position, speed=2) \ No newline at end of file diff --git a/actors/ghost_behaviors.py b/actors/ghost_behaviors.py new file mode 100644 index 0000000..87da6ce --- /dev/null +++ b/actors/ghost_behaviors.py @@ -0,0 +1,38 @@ +import pygame +import random + +def blinky_behavior(ghost, pacman, maze): + # Simple chase: move toward Pac-Man's position + return path_toward(ghost, pacman.rect.center, maze) + +def pinky_behavior(ghost, pacman, maze): + # Aim 4 tiles ahead of Pac-Man + offset = pacman.direction * 64 + target = (pacman.rect.centerx + offset.x, pacman.rect.centery + offset.y) + return path_toward(ghost, target, maze) + +def inky_behavior(ghost, pacman, maze): + # Placeholder for now: same as Pinky + return pinky_behavior(ghost, pacman, maze) + +def clyde_behavior(ghost, pacman, maze): + # If close to Pac-Man, scatter; otherwise chase + distance = pygame.Vector2(pacman.rect.center).distance_to(ghost.rect.center) + if distance < 100: + return path_toward(ghost, ghost.home_position, maze) + return path_toward(ghost, pacman.rect.center, maze) + +def path_toward(ghost, target_pos, maze): + # Placeholder logic: pick a direction that reduces distance to target + directions = [pygame.Vector2(1, 0), pygame.Vector2(-1, 0), + pygame.Vector2(0, 1), pygame.Vector2(0, -1)] + best = ghost.direction + min_dist = float("inf") + for d in directions: + test_pos = ghost.rect.move(d.x * ghost.speed, d.y * ghost.speed) + if not maze.is_wall(test_pos.center): + dist = pygame.Vector2(target_pos).distance_to(test_pos.center) + if dist < min_dist: + min_dist = dist + best = d + return best \ No newline at end of file diff --git a/actors/ghost_mode_controller.py b/actors/ghost_mode_controller.py new file mode 100644 index 0000000..634d232 --- /dev/null +++ b/actors/ghost_mode_controller.py @@ -0,0 +1,40 @@ +import time +from enums import GhostMode + +class GhostModeController: + def __init__(self): + self.timers = [ + (GhostMode.SCATTER, 7), + (GhostMode.CHASE, 20), + (GhostMode.SCATTER, 7), + (GhostMode.CHASE, 20), + (GhostMode.SCATTER, 5), + (GhostMode.CHASE, 9999) # stay in chase mode eventually + ] + self.current_index = 0 + self.last_switch_time = time.time() + self.mode = self.timers[self.current_index][0] + self.frightened_until = 0 + + def update(self): + now = time.time() + + if self.mode == GhostMode.FRIGHTENED: + if now > self.frightened_until: + self.resume_cycle() + return + + mode, duration = self.timers[self.current_index] + if now - self.last_switch_time >= duration: + self.current_index = min(self.current_index + 1, len(self.timers) - 1) + self.mode = self.timers[self.current_index][0] + self.last_switch_time = now + + def trigger_frightened(self, duration=6): + self.mode = GhostMode.FRIGHTENED + self.frightened_until = time.time() + duration + + def resume_cycle(self): + # Return to normal cycle after frightened ends + self.mode = self.timers[self.current_index][0] + self.last_switch_time = time.time() \ No newline at end of file diff --git a/pman.py b/pman.py index 210573b..c8e2cfb 100644 --- a/pman.py +++ b/pman.py @@ -2,8 +2,29 @@ import pygame from actors.enums import Colors, PlayerDirection from actors.pacman import ActorPacman from labyrinth import Labyrinth +from actors.ghosts import Blinky, Pinky, Inky, Clyde # adjust import path as needed -__version__ = "0.1.2" +__version__ = "0.2.0" + + +def spawn_ghosts(center_position): + """ + Spawns up to 4 ghosts at the center of the maze ("ghost house"). + + Args: + center_position (tuple): (x, y) coordinates for spawn point. + + Returns: + pygame.sprite.Group: A group containing all ghost instances. + """ + offset = 20 # spread out a little bit inside ghost home + + blinky = Blinky((center_position[0] - offset, center_position[1] - offset)) + pinky = Pinky((center_position[0] + offset, center_position[1] - offset)) + inky = Inky((center_position[0] - offset, center_position[1] + offset)) + clyde = Clyde((center_position[0] + offset, center_position[1] + offset)) + + return pygame.sprite.Group(blinky, pinky, inky, clyde) def main() -> None: pygame.init() @@ -13,6 +34,8 @@ def main() -> None: labyrinth = Labyrinth(screen, width=screen_width, height=screen_height) player = ActorPacman(screen, center=(200, 200)) + ghost_home_center = (maze_width // 2, maze_height // 2) + ghosts = spawn_ghosts(ghost_home_center) clock = pygame.time.Clock() running = True @@ -47,6 +70,14 @@ def main() -> None: labyrinth.draw() player.animate() player.draw() + ghost_mode_controller.update() + current_mode = ghost_mode_controller.mode + + for ghost in ghosts: + ghost.set_mode(current_mode) + ghost.update(maze, pacman) + + ghosts.draw(screen) pygame.display.flip() clock.tick(60)