Coverage for src/glomph/game.py: 0%
146 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-06 00:58 +0800
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-06 00:58 +0800
1"""Game state and logic for Glomph Maze."""
3from enum import Enum
5from glomph.entities import Player, Position, create_ghosts
6from glomph.loaders import MazeData
9class GameState(Enum):
10 """Current state of the game."""
11 MENU = "menu"
12 PLAYING = "playing"
13 PAUSED = "paused"
14 GAME_OVER = "game_over"
15 VICTORY = "victory"
18class Game:
19 """Main game state manager."""
21 def __init__(self, maze_width: int = 28, maze_height: int = 31) -> None:
22 """Initialize game state."""
23 self.state = GameState.MENU
24 self.level = 1
25 self.maze_width = maze_width
26 self.maze_height = maze_height
28 # Initialize player at center-bottom of maze
29 start_x = maze_width // 2
30 start_y = maze_height - 2
31 self.player = Player(Position(start_x, start_y))
33 self.dots_remaining = 0
34 self.ghosts = create_ghosts()
36 # Maze collision data
37 self.maze_data: MazeData | None = None
38 self.collision_map: list[list[str]] = []
39 self.dot_map: list[list[str]] = []
41 def load_maze(self, maze_data: MazeData) -> None:
42 """Load maze data and build collision maps."""
43 self.maze_data = maze_data
45 # Parse maze content into 2D arrays
46 lines = maze_data.content.split('\n')
47 self.collision_map = []
48 self.dot_map = []
49 self.dots_remaining = 0
51 for line in lines:
52 if not line.strip():
53 continue
55 collision_row = []
56 dot_row = []
58 for char in line:
59 if char == '#':
60 # Wall - collision
61 collision_row.append('#')
62 dot_row.append(' ')
63 elif char == '.':
64 # Dot - no collision, collectible
65 collision_row.append(' ')
66 dot_row.append('.')
67 self.dots_remaining += 1
68 elif char == 'o':
69 # Power pellet - no collision, collectible
70 collision_row.append(' ')
71 dot_row.append('o')
72 self.dots_remaining += 1
73 else:
74 # Empty space or other characters
75 collision_row.append(' ')
76 dot_row.append(' ')
78 self.collision_map.append(collision_row)
79 self.dot_map.append(dot_row)
81 # Ensure all rows have the same width
82 max_width = max(len(row) for row in self.collision_map) if self.collision_map else 0
83 for row in self.collision_map:
84 while len(row) < max_width:
85 row.append(' ')
86 for row in self.dot_map:
87 while len(row) < max_width:
88 row.append(' ')
90 def is_collision(self, x: int, y: int) -> bool:
91 """Check if position (x, y) has a collision."""
92 if not self.collision_map or y < 0 or y >= len(self.collision_map):
93 return True # Out of bounds = collision
95 row = self.collision_map[y]
96 if x < 0 or x >= len(row):
97 return True # Out of bounds = collision
99 return row[x] == '#'
101 def collect_dot(self, x: int, y: int) -> bool:
102 """Collect dot/power pellet at position (x, y). Returns True if collected."""
103 if not self.dot_map or y < 0 or y >= len(self.dot_map):
104 return False
106 row = self.dot_map[y]
107 if x < 0 or x >= len(row):
108 return False
110 if row[x] in ('.', 'o'):
111 # Collect the dot
112 row[x] = ' ' # Remove from map
113 self.dots_remaining -= 1
115 # Award points
116 points = 10 if row[x] == '.' else 50 # Power pellet worth more
117 self.player.add_score(points)
119 return True
121 return False
123 def start_game(self) -> None:
124 """Start a new game."""
125 self.state = GameState.PLAYING
126 self.level = 1
127 self.player.lives = 3
128 self.player.score = 0
130 def pause_game(self) -> None:
131 """Pause/unpause the game."""
132 if self.state == GameState.PLAYING:
133 self.state = GameState.PAUSED
134 elif self.state == GameState.PAUSED:
135 self.state = GameState.PLAYING
137 def game_over(self) -> None:
138 """End the game."""
139 self.state = GameState.GAME_OVER
141 def move_player(self, direction: str) -> None:
142 """Move player in specified direction."""
143 if self.state != GameState.PLAYING:
144 return
146 # Update player direction and velocity
147 self.player.set_direction(direction)
149 # Calculate new position
150 x, y = self.player.position.x, self.player.position.y
151 if direction == "up":
152 y -= 1
153 elif direction == "down":
154 y += 1
155 elif direction == "left":
156 x -= 1
157 elif direction == "right":
158 x += 1
160 # Check collision at new position
161 if not self.is_collision(x, y):
162 self.player.position = Position(x, y)
164 # Check for dot collection
165 self.collect_dot(x, y)
167 # Check win condition
168 if self.dots_remaining == 0:
169 self.state = GameState.VICTORY
171 def update(self) -> None:
172 """Update game state (called each frame)."""
173 if self.state != GameState.PLAYING:
174 return
176 # Update ghosts
177 self.update_ghosts()
179 # Check ghost-player collisions
180 self.check_ghost_collisions()
182 def update_ghosts(self) -> None:
183 """Update all ghost positions and AI."""
184 for ghost in self.ghosts:
185 # Update ghost AI based on mode
186 ghost.update_ai(self.player.position)
188 # Calculate movement based on target
189 direction = ghost.calculate_direction()
191 # Calculate new position
192 x, y = ghost.position.x, ghost.position.y
193 if direction == "up":
194 y -= 1
195 elif direction == "down":
196 y += 1
197 elif direction == "left":
198 x -= 1
199 elif direction == "right":
200 x += 1
202 # Check collision at new position
203 if not self.is_collision(x, y):
204 ghost.position = Position(x, y)
206 def check_ghost_collisions(self) -> None:
207 """Check for collisions between player and ghosts."""
208 for ghost in self.ghosts:
209 if (self.player.position.x == ghost.position.x and
210 self.player.position.y == ghost.position.y):
211 # Player hit a ghost
212 self.lose_life()
213 break
215 def is_game_won(self) -> bool:
216 """Check if the current level is won."""
217 return self.dots_remaining == 0
219 def next_level(self) -> None:
220 """Advance to the next level."""
221 self.level += 1
222 # Reset player position
223 start_x = self.maze_width // 2
224 start_y = self.maze_height - 2
225 self.player.position = Position(start_x, start_y)
226 # TODO: Load new maze
227 # TODO: Reset dots
228 # TODO: Reset ghosts
230 def lose_life(self) -> None:
231 """Player loses a life."""
232 self.player.lose_life()
234 if self.player.lives <= 0:
235 self.game_over()
236 else:
237 # Reset player position
238 start_x = self.maze_width // 2
239 start_y = self.maze_height - 2
240 self.player.position = Position(start_x, start_y)