Coverage for src/glomph/entities.py: 0%
75 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-06 00:21 +0800
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-06 00:21 +0800
1"""Game entities (player, ghosts, etc.)."""
3from enum import Enum
4from typing import NamedTuple
7class Position(NamedTuple):
8 """2D position coordinates."""
9 x: int
10 y: int
13class Velocity(NamedTuple):
14 """Movement velocity."""
15 dx: int
16 dy: int
19class Entity:
20 """Base class for game entities."""
22 def __init__(self, position: Position, char: str = "?", color: int = 0) -> None:
23 """Initialize entity."""
24 self.position = position
25 self.char = char
26 self.color = color
27 self.velocity = Velocity(0, 0)
29 def move(self, dx: int = 0, dy: int = 0) -> None:
30 """Move entity by delta."""
31 self.position = Position(
32 self.position.x + dx,
33 self.position.y + dy
34 )
36 def set_velocity(self, dx: int, dy: int) -> None:
37 """Set movement velocity."""
38 self.velocity = Velocity(dx, dy)
40 def update(self) -> None:
41 """Update entity state."""
42 self.move(self.velocity.dx, self.velocity.dy)
45class Player(Entity):
46 """Player entity (Pac-Man)."""
48 def __init__(self, position: Position) -> None:
49 """Initialize player."""
50 super().__init__(position, "@", 2) # Red @ symbol
51 self.lives = 3
52 self.score = 0
53 self.direction = "left" # Current facing direction
55 def set_direction(self, direction: str) -> None:
56 """Set player direction and update velocity."""
57 self.direction = direction
58 if direction == "up":
59 self.set_velocity(0, -1)
60 elif direction == "down":
61 self.set_velocity(0, 1)
62 elif direction == "left":
63 self.set_velocity(-1, 0)
64 elif direction == "right":
65 self.set_velocity(1, 0)
66 else:
67 self.set_velocity(0, 0)
69 def lose_life(self) -> None:
70 """Player loses a life."""
71 self.lives -= 1
73 def add_score(self, points: int) -> None:
74 """Add points to score."""
75 self.score += points
78class GhostMode(Enum):
79 """Ghost behavior modes."""
80 SCATTER = "scatter"
81 CHASE = "chase"
82 FRIGHTENED = "frightened"
85class Ghost(Entity):
86 """Ghost entity."""
88 def __init__(self, position: Position, name: str, color: int) -> None:
89 """Initialize ghost."""
90 super().__init__(position, "M", color) # M for ghost
91 self.name = name
92 self.mode = GhostMode.SCATTER
93 self.target = position # Position to move towards
94 self.home_corner = position # Scatter mode target corner
96 def set_mode(self, mode: GhostMode) -> None:
97 """Set ghost behavior mode."""
98 self.mode = mode
99 if mode == GhostMode.FRIGHTENED:
100 self.char = "Ø" # Different appearance when frightened
101 self.color = 5 # Magenta when frightened
102 else:
103 self.char = "M"
104 # Reset to normal color (would be per-ghost)
106 def set_target(self, target: Position) -> None:
107 """Set movement target position."""
108 self.target = target
110 def update_ai(self, player_pos: Position) -> None:
111 """Update ghost AI based on current mode."""
112 if self.mode == GhostMode.SCATTER:
113 self.set_target(self.home_corner)
114 elif self.mode == GhostMode.CHASE:
115 # Different chase behaviors per ghost (simplified for now)
116 self.set_target(player_pos)
117 elif self.mode == GhostMode.FRIGHTENED:
118 # Random movement when frightened (simplified)
119 pass
121 def calculate_direction(self) -> str:
122 """Calculate best direction to move towards target."""
123 # Simple pathfinding: move towards target
124 dx = self.target.x - self.position.x
125 dy = self.target.y - self.position.y
127 # Prefer larger axis difference
128 if abs(dx) > abs(dy):
129 return "right" if dx > 0 else "left"
130 else:
131 return "down" if dy > 0 else "up"
134def create_ghosts() -> list[Ghost]:
135 """Create the four classic ghosts."""
136 return [
137 Ghost(Position(14, 11), "Blinky", 2), # Red, targets player directly
138 Ghost(Position(14, 13), "Pinky", 5), # Pink, ambushes ahead of player
139 Ghost(Position(12, 13), "Inky", 4), # Cyan, uses complex targeting
140 Ghost(Position(16, 13), "Clyde", 3), # Orange, random when close
141 ]