"""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 # 1 = wall, . = dot, o = power pellet, ' ' = empty, P = pacman start, G = ghost start 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 # Only pick a new direction when aligned with tile center 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) # avoid reversing unless dead end 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: # chase: prefer direction reducing distance to pacman 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 # try to turn 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 # snap to lane center for smooth turning 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) # pacman with animated mouth 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) # cut mouth with black wedge 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), ]) # ghosts 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 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()