pacman-python / pacman.py
ruthlesslearner's picture
Add Pac-Man pygame game
804cae4 verified
Raw
History Blame Contribute Delete
11.5 kB
"""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()