| """Pac-Man in Python (pygame). Run: python3 pacman.py""" |
| import random |
| import pygame |
|
|
| TILE = 24 |
| ROWS = 21 |
| COLS = 21 |
| FPS = 60 |
| GHOST_SPEED = 2 |
| PAC_SPEED = 3 |
| GHOST_FRIGHT_SPEED = 1 |
|
|
| |
| MAZE = [ |
| "#####################", |
| "#.........#.........#", |
| "#o###.###.#.###.###o#", |
| "#...................#", |
| "#.###.#.#####.#.###.#", |
| "#.....#...#...#.....#", |
| "#####.###.#.###.#####", |
| " #.#.......#.# ", |
| "#####.##.###.##.#####", |
| "........#G G#........", |
| "#####.##.###.##.#####", |
| " #.#.......#.# ", |
| "#####.##.###.##.#####", |
| "#.........#.........#", |
| "#.###.###.#.###.###.#", |
| "#o..#.....P.....#..o#", |
| "###.#.#.#####.#.#.###", |
| "#.....#...#...#.....#", |
| "#.#######.#.#######.#", |
| "#...................#", |
| "#####################", |
| ] |
|
|
| BLACK = (0, 0, 0) |
| BLUE = (33, 33, 222) |
| YELLOW = (255, 255, 0) |
| WHITE = (255, 255, 255) |
| GHOST_COLORS = [(255, 0, 0), (255, 153, 255), (0, 255, 255), (255, 184, 82)] |
|
|
| DIRS = {pygame.K_LEFT: (-1, 0), pygame.K_RIGHT: (1, 0), |
| pygame.K_UP: (0, -1), pygame.K_DOWN: (0, 1)} |
| DIR_NAMES = {(-1, 0): "L", (1, 0): "R", (0, -1): "U", (0, 1): "D"} |
|
|
|
|
| def is_wall(col, row): |
| if 0 <= row < ROWS and 0 <= col < COLS: |
| return MAZE[row][col] == "#" |
| return True |
|
|
|
|
| def is_playable(col, row): |
| if 0 <= row < ROWS and 0 <= col < COLS: |
| return MAZE[row][col] != "#" |
| return False |
|
|
|
|
| class Ghost: |
| def __init__(self, col, row, color, name): |
| self.start = (col * TILE + TILE // 2, row * TILE + TILE // 2) |
| self.color = color |
| self.name = name |
| self.reset() |
|
|
| def reset(self): |
| self.x, self.y = self.start |
| self.dir = random.choice([(-1, 0), (1, 0)]) |
| self.frightened = False |
|
|
| def rect(self): |
| return pygame.Rect(self.x - TILE // 2 + 2, self.y - TILE // 2 + 2, |
| TILE - 4, TILE - 4) |
|
|
| def center_tile(self): |
| return (int(self.x // TILE), int(self.y // TILE)) |
|
|
| def update(self, pac_tile, dots_left): |
| speed = GHOST_FRIGHT_SPEED if self.frightened else GHOST_SPEED |
|
|
| |
| cx, cy = self.center_tile() |
| tile_center_x = cx * TILE + TILE // 2 |
| tile_center_y = cy * TILE + TILE // 2 |
| at_center = abs(self.x - tile_center_x) < speed and abs(self.y - tile_center_y) < speed |
| if at_center: |
| self.x, self.y = tile_center_x, tile_center_y |
| options = [] |
| for d in DIRS.values(): |
| nx, ny = cx + d[0], cy + d[1] |
| if is_playable(nx, ny): |
| options.append(d) |
| |
| back = (-self.dir[0], -self.dir[1]) |
| if back in options and len(options) > 1: |
| options.remove(back) |
| if options: |
| if self.frightened: |
| choice = random.choice(options) |
| else: |
| |
| choice = min( |
| options, |
| key=lambda d: (pac_tile[0] - (cx + d[0])) ** 2 |
| + (pac_tile[1] - (cy + d[1])) ** 2, |
| ) |
| self.dir = choice |
|
|
| self.x += self.dir[0] * speed |
| self.y += self.dir[1] * speed |
|
|
|
|
| class Game: |
| def __init__(self): |
| pygame.init() |
| self.screen = pygame.display.set_mode((COLS * TILE, ROWS * TILE + 40)) |
| pygame.display.set_caption("Pac-Man") |
| self.clock = pygame.time.Clock() |
| self.font = pygame.font.SysFont("arial", 20, bold=True) |
| self.big_font = pygame.font.SysFont("arial", 36, bold=True) |
| self.reset_level() |
| self.score = 0 |
| self.lives = 3 |
|
|
| def reset_level(self): |
| self.dots = set() |
| self.pellets = set() |
| pac_start = (10 * TILE + TILE // 2, 15 * TILE + TILE // 2) |
| for r, line in enumerate(MAZE): |
| for c, ch in enumerate(line): |
| if ch == ".": |
| self.dots.add((c, r)) |
| elif ch == "o": |
| self.pellets.add((c, r)) |
| elif ch == "P": |
| pac_start = (c * TILE + TILE // 2, r * TILE + TILE // 2) |
| self.pac = pygame.Rect(pac_start[0] - TILE // 2 + 2, |
| pac_start[1] - TILE // 2 + 2, TILE - 4, TILE - 4) |
| self.pac_dir = (-1, 0) |
| self.pac_want = (-1, 0) |
| self.mouth = 0 |
| self.ghosts = [ |
| Ghost(9, 9, GHOST_COLORS[0], "blinky"), |
| Ghost(11, 9, GHOST_COLORS[1], "pinky"), |
| Ghost(9, 11, GHOST_COLORS[2], "inky"), |
| Ghost(11, 11, GHOST_COLORS[3], "clyde"), |
| ] |
| self.fright_timer = 0 |
|
|
| def move_pac(self): |
| speed = PAC_SPEED |
| dx, dy = self.pac_want |
| |
| test = self.pac.copy() |
| test.x += dx * speed |
| test.y += dy * speed |
| ahead_col = int((self.pac.centerx + dx * TILE // 2) // TILE) |
| ahead_row = int((self.pac.centery + dy * TILE // 2) // TILE) |
| if not is_wall(ahead_col, ahead_row): |
| self.pac_dir = self.pac_want |
| dx, dy = self.pac_dir |
| ahead_col = int((self.pac.centerx + dx * speed + dx * TILE // 3) // TILE) |
| ahead_row = int((self.pac.centery + dy * speed + dy * TILE // 3) // TILE) |
| if not is_wall(ahead_col, ahead_row): |
| self.pac.x += dx * speed |
| self.pac.y += dy * speed |
| |
| if dx != 0: |
| target = (self.pac.centery // TILE) * TILE + TILE // 2 |
| self.pac.centery += max(-speed, min(speed, target - self.pac.centery)) |
| else: |
| target = (self.pac.centerx // TILE) * TILE + TILE // 2 |
| self.pac.centerx += max(-speed, min(speed, target - self.pac.centerx)) |
|
|
| def eat(self): |
| cx, cy = self.pac.centerx // TILE, self.pac.centery // TILE |
| pos = (cx, cy) |
| if pos in self.dots: |
| self.dots.remove(pos) |
| self.score += 10 |
| elif pos in self.pellets: |
| self.pellets.remove(pos) |
| self.score += 50 |
| self.fright_timer = 400 |
| for g in self.ghosts: |
| g.frightened = True |
|
|
| def draw(self): |
| self.screen.fill(BLACK) |
| for r in range(ROWS): |
| for c in range(COLS): |
| if is_wall(c, r): |
| pygame.draw.rect(self.screen, BLUE, |
| (c * TILE, r * TILE, TILE, TILE), 2) |
| for (c, r) in self.dots: |
| pygame.draw.circle(self.screen, WHITE, |
| (c * TILE + TILE // 2, r * TILE + TILE // 2), 3) |
| for (c, r) in self.pellets: |
| pygame.draw.circle(self.screen, WHITE, |
| (c * TILE + TILE // 2, r * TILE + TILE // 2), 7) |
| |
| angle_map = {(-1, 0): 180, (1, 0): 0, (0, -1): 90, (0, 1): 270} |
| base = angle_map[self.pac_dir] |
| mouth = abs(self.mouth) * 25 |
| pygame.draw.circle(self.screen, YELLOW, self.pac.center, TILE // 2 - 2) |
| |
| pygame.draw.polygon(self.screen, BLACK, [ |
| self.pac.center, |
| (self.pac.centerx + (TILE // 2) * pygame.math.Vector2(1, 0).rotate(-base + mouth).x, |
| self.pac.centery + (TILE // 2) * pygame.math.Vector2(1, 0).rotate(-base + mouth).y), |
| (self.pac.centerx + (TILE // 2) * pygame.math.Vector2(1, 0).rotate(-base - mouth).x, |
| self.pac.centery + (TILE // 2) * pygame.math.Vector2(1, 0).rotate(-base - mouth).y), |
| ]) |
| |
| for g in self.ghosts: |
| color = (0, 0, 255) if g.frightened else g.color |
| r = g.rect() |
| pygame.draw.circle(self.screen, color, (r.centerx, r.centery - 3), r.width // 2) |
| pygame.draw.rect(self.screen, color, (r.x, r.centery - 3, r.width, r.height // 2 + 2)) |
| pygame.draw.circle(self.screen, WHITE, (r.centerx - 5, r.centery - 4), 4) |
| pygame.draw.circle(self.screen, WHITE, (r.centerx + 5, r.centery - 4), 4) |
| pygame.draw.circle(self.screen, (0, 0, 139), |
| (r.centerx - 5 + g.dir[0] * 2, r.centery - 4 + g.dir[1] * 2), 2) |
| pygame.draw.circle(self.screen, (0, 0, 139), |
| (r.centerx + 5 + g.dir[0] * 2, r.centery - 4 + g.dir[1] * 2), 2) |
| |
| hud = self.font.render(f"Score: {self.score} Lives: {self.lives}", True, WHITE) |
| self.screen.blit(hud, (8, ROWS * TILE + 10)) |
| if self.fright_timer > 0: |
| ft = self.font.render("POWER!", True, (255, 255, 0)) |
| self.screen.blit(ft, (COLS * TILE - 90, ROWS * TILE + 10)) |
| pygame.display.flip() |
|
|
| def game_over_screen(self, won): |
| msg = "YOU WIN!" if won else "GAME OVER" |
| text = self.big_font.render(msg, True, YELLOW if won else (255, 0, 0)) |
| rect = text.get_rect(center=(COLS * TILE // 2, ROWS * TILE // 2)) |
| self.screen.blit(text, rect) |
| sub = self.font.render("Press R to restart, Q to quit", True, WHITE) |
| self.screen.blit(sub, sub.get_rect(center=(COLS * TILE // 2, ROWS * TILE // 2 + 45))) |
| pygame.display.flip() |
| while True: |
| for e in pygame.event.get(): |
| if e.type == pygame.QUIT: |
| return False |
| if e.type == pygame.KEYDOWN: |
| if e.key == pygame.K_r: |
| self.score = 0 |
| self.lives = 3 |
| self.reset_level() |
| return True |
| if e.key == pygame.K_q: |
| return False |
|
|
| def run(self): |
| running = True |
| while running: |
| self.clock.tick(FPS) |
| for e in pygame.event.get(): |
| if e.type == pygame.QUIT: |
| running = False |
| elif e.type == pygame.KEYDOWN: |
| if e.key in DIRS: |
| self.pac_want = DIRS[e.key] |
| elif e.key == pygame.K_ESCAPE: |
| running = False |
|
|
| self.move_pac() |
| self.mouth = (self.mouth + 0.2) % 3 |
|
|
| self.eat() |
|
|
| pac_tile = (self.pac.centerx // TILE, self.pac.centery // TILE) |
| for g in self.ghosts: |
| g.update(pac_tile, len(self.dots)) |
| if g.rect().colliderect(self.pac): |
| if g.frightened: |
| self.score += 200 |
| g.reset() |
| g.frightened = False |
| else: |
| self.lives -= 1 |
| if self.lives <= 0: |
| running = self.game_over_screen(False) |
| else: |
| self.reset_level() |
| break |
|
|
| if self.fright_timer > 0: |
| self.fright_timer -= 1 |
| if self.fright_timer == 0: |
| for g in self.ghosts: |
| g.frightened = False |
|
|
| if not self.dots and not self.pellets: |
| running = self.game_over_screen(True) |
|
|
| self.draw() |
| pygame.quit() |
|
|
|
|
| if __name__ == "__main__": |
| Game().run() |
|
|