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

1"""Game state and logic for Glomph Maze.""" 

2 

3from enum import Enum 

4 

5from glomph.entities import Player, Position, create_ghosts 

6from glomph.loaders import MazeData 

7 

8 

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" 

16 

17 

18class Game: 

19 """Main game state manager.""" 

20 

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 

27 

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)) 

32 

33 self.dots_remaining = 0 

34 self.ghosts = create_ghosts() 

35 

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]] = [] 

40 

41 def load_maze(self, maze_data: MazeData) -> None: 

42 """Load maze data and build collision maps.""" 

43 self.maze_data = maze_data 

44 

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 

50 

51 for line in lines: 

52 if not line.strip(): 

53 continue 

54 

55 collision_row = [] 

56 dot_row = [] 

57 

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(' ') 

77 

78 self.collision_map.append(collision_row) 

79 self.dot_map.append(dot_row) 

80 

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(' ') 

89 

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 

94 

95 row = self.collision_map[y] 

96 if x < 0 or x >= len(row): 

97 return True # Out of bounds = collision 

98 

99 return row[x] == '#' 

100 

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 

105 

106 row = self.dot_map[y] 

107 if x < 0 or x >= len(row): 

108 return False 

109 

110 if row[x] in ('.', 'o'): 

111 # Collect the dot 

112 row[x] = ' ' # Remove from map 

113 self.dots_remaining -= 1 

114 

115 # Award points 

116 points = 10 if row[x] == '.' else 50 # Power pellet worth more 

117 self.player.add_score(points) 

118 

119 return True 

120 

121 return False 

122 

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 

129 

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 

136 

137 def game_over(self) -> None: 

138 """End the game.""" 

139 self.state = GameState.GAME_OVER 

140 

141 def move_player(self, direction: str) -> None: 

142 """Move player in specified direction.""" 

143 if self.state != GameState.PLAYING: 

144 return 

145 

146 # Update player direction and velocity 

147 self.player.set_direction(direction) 

148 

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 

159 

160 # Check collision at new position 

161 if not self.is_collision(x, y): 

162 self.player.position = Position(x, y) 

163 

164 # Check for dot collection 

165 self.collect_dot(x, y) 

166 

167 # Check win condition 

168 if self.dots_remaining == 0: 

169 self.state = GameState.VICTORY 

170 

171 def update(self) -> None: 

172 """Update game state (called each frame).""" 

173 if self.state != GameState.PLAYING: 

174 return 

175 

176 # Update ghosts 

177 self.update_ghosts() 

178 

179 # Check ghost-player collisions 

180 self.check_ghost_collisions() 

181 

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) 

187 

188 # Calculate movement based on target 

189 direction = ghost.calculate_direction() 

190 

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 

201 

202 # Check collision at new position 

203 if not self.is_collision(x, y): 

204 ghost.position = Position(x, y) 

205 

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 

214 

215 def is_game_won(self) -> bool: 

216 """Check if the current level is won.""" 

217 return self.dots_remaining == 0 

218 

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 

229 

230 def lose_life(self) -> None: 

231 """Player loses a life.""" 

232 self.player.lose_life() 

233 

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)