ruthlesslearner commited on
Commit
804cae4
·
verified ·
1 Parent(s): d0de357

Add Pac-Man pygame game

Browse files
Files changed (2) hide show
  1. README.md +28 -0
  2. pacman.py +305 -0
README.md ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ title: Pac-Man (Python/pygame)
4
+ ---
5
+
6
+ # Pac-Man in Python
7
+
8
+ A classic Pac-Man clone written in ~250 lines of Python using pygame.
9
+
10
+ ## Run locally
11
+
12
+ ```bash
13
+ pip install pygame
14
+ python pacman.py
15
+ ```
16
+
17
+ ## Controls
18
+
19
+ - **Arrow keys** — move
20
+ - **R** — restart after game over
21
+ - **Q** or **Esc** — quit
22
+
23
+ ## Features
24
+
25
+ - Maze with dots (10 pts) and power pellets (50 pts)
26
+ - Power pellets make ghosts frightened: they turn blue, slow down, and are worth 200 pts when eaten
27
+ - 4 ghosts with simple chase AI (they pick the direction toward Pac-Man at each intersection)
28
+ - 3 lives, win screen when the maze is cleared
pacman.py ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pac-Man in Python (pygame). Run: python3 pacman.py"""
2
+ import random
3
+ import pygame
4
+
5
+ TILE = 24
6
+ ROWS = 21
7
+ COLS = 21
8
+ FPS = 60
9
+ GHOST_SPEED = 2
10
+ PAC_SPEED = 3
11
+ GHOST_FRIGHT_SPEED = 1
12
+
13
+ # 1 = wall, . = dot, o = power pellet, ' ' = empty, P = pacman start, G = ghost start
14
+ MAZE = [
15
+ "#####################",
16
+ "#.........#.........#",
17
+ "#o###.###.#.###.###o#",
18
+ "#...................#",
19
+ "#.###.#.#####.#.###.#",
20
+ "#.....#...#...#.....#",
21
+ "#####.###.#.###.#####",
22
+ " #.#.......#.# ",
23
+ "#####.##.###.##.#####",
24
+ "........#G G#........",
25
+ "#####.##.###.##.#####",
26
+ " #.#.......#.# ",
27
+ "#####.##.###.##.#####",
28
+ "#.........#.........#",
29
+ "#.###.###.#.###.###.#",
30
+ "#o..#.....P.....#..o#",
31
+ "###.#.#.#####.#.#.###",
32
+ "#.....#...#...#.....#",
33
+ "#.#######.#.#######.#",
34
+ "#...................#",
35
+ "#####################",
36
+ ]
37
+
38
+ BLACK = (0, 0, 0)
39
+ BLUE = (33, 33, 222)
40
+ YELLOW = (255, 255, 0)
41
+ WHITE = (255, 255, 255)
42
+ GHOST_COLORS = [(255, 0, 0), (255, 153, 255), (0, 255, 255), (255, 184, 82)]
43
+
44
+ DIRS = {pygame.K_LEFT: (-1, 0), pygame.K_RIGHT: (1, 0),
45
+ pygame.K_UP: (0, -1), pygame.K_DOWN: (0, 1)}
46
+ DIR_NAMES = {(-1, 0): "L", (1, 0): "R", (0, -1): "U", (0, 1): "D"}
47
+
48
+
49
+ def is_wall(col, row):
50
+ if 0 <= row < ROWS and 0 <= col < COLS:
51
+ return MAZE[row][col] == "#"
52
+ return True
53
+
54
+
55
+ def is_playable(col, row):
56
+ if 0 <= row < ROWS and 0 <= col < COLS:
57
+ return MAZE[row][col] != "#"
58
+ return False
59
+
60
+
61
+ class Ghost:
62
+ def __init__(self, col, row, color, name):
63
+ self.start = (col * TILE + TILE // 2, row * TILE + TILE // 2)
64
+ self.color = color
65
+ self.name = name
66
+ self.reset()
67
+
68
+ def reset(self):
69
+ self.x, self.y = self.start
70
+ self.dir = random.choice([(-1, 0), (1, 0)])
71
+ self.frightened = False
72
+
73
+ def rect(self):
74
+ return pygame.Rect(self.x - TILE // 2 + 2, self.y - TILE // 2 + 2,
75
+ TILE - 4, TILE - 4)
76
+
77
+ def center_tile(self):
78
+ return (int(self.x // TILE), int(self.y // TILE))
79
+
80
+ def update(self, pac_tile, dots_left):
81
+ speed = GHOST_FRIGHT_SPEED if self.frightened else GHOST_SPEED
82
+
83
+ # Only pick a new direction when aligned with tile center
84
+ cx, cy = self.center_tile()
85
+ tile_center_x = cx * TILE + TILE // 2
86
+ tile_center_y = cy * TILE + TILE // 2
87
+ at_center = abs(self.x - tile_center_x) < speed and abs(self.y - tile_center_y) < speed
88
+ if at_center:
89
+ self.x, self.y = tile_center_x, tile_center_y
90
+ options = []
91
+ for d in DIRS.values():
92
+ nx, ny = cx + d[0], cy + d[1]
93
+ if is_playable(nx, ny):
94
+ options.append(d)
95
+ # avoid reversing unless dead end
96
+ back = (-self.dir[0], -self.dir[1])
97
+ if back in options and len(options) > 1:
98
+ options.remove(back)
99
+ if options:
100
+ if self.frightened:
101
+ choice = random.choice(options)
102
+ else:
103
+ # chase: prefer direction reducing distance to pacman
104
+ choice = min(
105
+ options,
106
+ key=lambda d: (pac_tile[0] - (cx + d[0])) ** 2
107
+ + (pac_tile[1] - (cy + d[1])) ** 2,
108
+ )
109
+ self.dir = choice
110
+
111
+ self.x += self.dir[0] * speed
112
+ self.y += self.dir[1] * speed
113
+
114
+
115
+ class Game:
116
+ def __init__(self):
117
+ pygame.init()
118
+ self.screen = pygame.display.set_mode((COLS * TILE, ROWS * TILE + 40))
119
+ pygame.display.set_caption("Pac-Man")
120
+ self.clock = pygame.time.Clock()
121
+ self.font = pygame.font.SysFont("arial", 20, bold=True)
122
+ self.big_font = pygame.font.SysFont("arial", 36, bold=True)
123
+ self.reset_level()
124
+ self.score = 0
125
+ self.lives = 3
126
+
127
+ def reset_level(self):
128
+ self.dots = set()
129
+ self.pellets = set()
130
+ pac_start = (10 * TILE + TILE // 2, 15 * TILE + TILE // 2)
131
+ for r, line in enumerate(MAZE):
132
+ for c, ch in enumerate(line):
133
+ if ch == ".":
134
+ self.dots.add((c, r))
135
+ elif ch == "o":
136
+ self.pellets.add((c, r))
137
+ elif ch == "P":
138
+ pac_start = (c * TILE + TILE // 2, r * TILE + TILE // 2)
139
+ self.pac = pygame.Rect(pac_start[0] - TILE // 2 + 2,
140
+ pac_start[1] - TILE // 2 + 2, TILE - 4, TILE - 4)
141
+ self.pac_dir = (-1, 0)
142
+ self.pac_want = (-1, 0)
143
+ self.mouth = 0
144
+ self.ghosts = [
145
+ Ghost(9, 9, GHOST_COLORS[0], "blinky"),
146
+ Ghost(11, 9, GHOST_COLORS[1], "pinky"),
147
+ Ghost(9, 11, GHOST_COLORS[2], "inky"),
148
+ Ghost(11, 11, GHOST_COLORS[3], "clyde"),
149
+ ]
150
+ self.fright_timer = 0
151
+
152
+ def move_pac(self):
153
+ speed = PAC_SPEED
154
+ dx, dy = self.pac_want
155
+ # try to turn
156
+ test = self.pac.copy()
157
+ test.x += dx * speed
158
+ test.y += dy * speed
159
+ ahead_col = int((self.pac.centerx + dx * TILE // 2) // TILE)
160
+ ahead_row = int((self.pac.centery + dy * TILE // 2) // TILE)
161
+ if not is_wall(ahead_col, ahead_row):
162
+ self.pac_dir = self.pac_want
163
+ dx, dy = self.pac_dir
164
+ ahead_col = int((self.pac.centerx + dx * speed + dx * TILE // 3) // TILE)
165
+ ahead_row = int((self.pac.centery + dy * speed + dy * TILE // 3) // TILE)
166
+ if not is_wall(ahead_col, ahead_row):
167
+ self.pac.x += dx * speed
168
+ self.pac.y += dy * speed
169
+ # snap to lane center for smooth turning
170
+ if dx != 0:
171
+ target = (self.pac.centery // TILE) * TILE + TILE // 2
172
+ self.pac.centery += max(-speed, min(speed, target - self.pac.centery))
173
+ else:
174
+ target = (self.pac.centerx // TILE) * TILE + TILE // 2
175
+ self.pac.centerx += max(-speed, min(speed, target - self.pac.centerx))
176
+
177
+ def eat(self):
178
+ cx, cy = self.pac.centerx // TILE, self.pac.centery // TILE
179
+ pos = (cx, cy)
180
+ if pos in self.dots:
181
+ self.dots.remove(pos)
182
+ self.score += 10
183
+ elif pos in self.pellets:
184
+ self.pellets.remove(pos)
185
+ self.score += 50
186
+ self.fright_timer = 400
187
+ for g in self.ghosts:
188
+ g.frightened = True
189
+
190
+ def draw(self):
191
+ self.screen.fill(BLACK)
192
+ for r in range(ROWS):
193
+ for c in range(COLS):
194
+ if is_wall(c, r):
195
+ pygame.draw.rect(self.screen, BLUE,
196
+ (c * TILE, r * TILE, TILE, TILE), 2)
197
+ for (c, r) in self.dots:
198
+ pygame.draw.circle(self.screen, WHITE,
199
+ (c * TILE + TILE // 2, r * TILE + TILE // 2), 3)
200
+ for (c, r) in self.pellets:
201
+ pygame.draw.circle(self.screen, WHITE,
202
+ (c * TILE + TILE // 2, r * TILE + TILE // 2), 7)
203
+ # pacman with animated mouth
204
+ angle_map = {(-1, 0): 180, (1, 0): 0, (0, -1): 90, (0, 1): 270}
205
+ base = angle_map[self.pac_dir]
206
+ mouth = abs(self.mouth) * 25
207
+ pygame.draw.circle(self.screen, YELLOW, self.pac.center, TILE // 2 - 2)
208
+ # cut mouth with black wedge
209
+ pygame.draw.polygon(self.screen, BLACK, [
210
+ self.pac.center,
211
+ (self.pac.centerx + (TILE // 2) * pygame.math.Vector2(1, 0).rotate(-base + mouth).x,
212
+ self.pac.centery + (TILE // 2) * pygame.math.Vector2(1, 0).rotate(-base + mouth).y),
213
+ (self.pac.centerx + (TILE // 2) * pygame.math.Vector2(1, 0).rotate(-base - mouth).x,
214
+ self.pac.centery + (TILE // 2) * pygame.math.Vector2(1, 0).rotate(-base - mouth).y),
215
+ ])
216
+ # ghosts
217
+ for g in self.ghosts:
218
+ color = (0, 0, 255) if g.frightened else g.color
219
+ r = g.rect()
220
+ pygame.draw.circle(self.screen, color, (r.centerx, r.centery - 3), r.width // 2)
221
+ pygame.draw.rect(self.screen, color, (r.x, r.centery - 3, r.width, r.height // 2 + 2))
222
+ pygame.draw.circle(self.screen, WHITE, (r.centerx - 5, r.centery - 4), 4)
223
+ pygame.draw.circle(self.screen, WHITE, (r.centerx + 5, r.centery - 4), 4)
224
+ pygame.draw.circle(self.screen, (0, 0, 139),
225
+ (r.centerx - 5 + g.dir[0] * 2, r.centery - 4 + g.dir[1] * 2), 2)
226
+ pygame.draw.circle(self.screen, (0, 0, 139),
227
+ (r.centerx + 5 + g.dir[0] * 2, r.centery - 4 + g.dir[1] * 2), 2)
228
+ # HUD
229
+ hud = self.font.render(f"Score: {self.score} Lives: {self.lives}", True, WHITE)
230
+ self.screen.blit(hud, (8, ROWS * TILE + 10))
231
+ if self.fright_timer > 0:
232
+ ft = self.font.render("POWER!", True, (255, 255, 0))
233
+ self.screen.blit(ft, (COLS * TILE - 90, ROWS * TILE + 10))
234
+ pygame.display.flip()
235
+
236
+ def game_over_screen(self, won):
237
+ msg = "YOU WIN!" if won else "GAME OVER"
238
+ text = self.big_font.render(msg, True, YELLOW if won else (255, 0, 0))
239
+ rect = text.get_rect(center=(COLS * TILE // 2, ROWS * TILE // 2))
240
+ self.screen.blit(text, rect)
241
+ sub = self.font.render("Press R to restart, Q to quit", True, WHITE)
242
+ self.screen.blit(sub, sub.get_rect(center=(COLS * TILE // 2, ROWS * TILE // 2 + 45)))
243
+ pygame.display.flip()
244
+ while True:
245
+ for e in pygame.event.get():
246
+ if e.type == pygame.QUIT:
247
+ return False
248
+ if e.type == pygame.KEYDOWN:
249
+ if e.key == pygame.K_r:
250
+ self.score = 0
251
+ self.lives = 3
252
+ self.reset_level()
253
+ return True
254
+ if e.key == pygame.K_q:
255
+ return False
256
+
257
+ def run(self):
258
+ running = True
259
+ while running:
260
+ self.clock.tick(FPS)
261
+ for e in pygame.event.get():
262
+ if e.type == pygame.QUIT:
263
+ running = False
264
+ elif e.type == pygame.KEYDOWN:
265
+ if e.key in DIRS:
266
+ self.pac_want = DIRS[e.key]
267
+ elif e.key == pygame.K_ESCAPE:
268
+ running = False
269
+
270
+ self.move_pac()
271
+ self.mouth = (self.mouth + 0.2) % 3
272
+
273
+ self.eat()
274
+
275
+ pac_tile = (self.pac.centerx // TILE, self.pac.centery // TILE)
276
+ for g in self.ghosts:
277
+ g.update(pac_tile, len(self.dots))
278
+ if g.rect().colliderect(self.pac):
279
+ if g.frightened:
280
+ self.score += 200
281
+ g.reset()
282
+ g.frightened = False
283
+ else:
284
+ self.lives -= 1
285
+ if self.lives <= 0:
286
+ running = self.game_over_screen(False)
287
+ else:
288
+ self.reset_level()
289
+ break
290
+
291
+ if self.fright_timer > 0:
292
+ self.fright_timer -= 1
293
+ if self.fright_timer == 0:
294
+ for g in self.ghosts:
295
+ g.frightened = False
296
+
297
+ if not self.dots and not self.pellets:
298
+ running = self.game_over_screen(True)
299
+
300
+ self.draw()
301
+ pygame.quit()
302
+
303
+
304
+ if __name__ == "__main__":
305
+ Game().run()