基于pygame实现的吃豆人游戏代码

目录

1. 介绍

2. 代码结构概览

3. 核心功能解析

1. 迷宫生成算法(Prim算法)

2. 幽灵AI:A*路径追踪

3. 吃豆人控制与动画

4. 游戏逻辑与状态管理

4. 完整代码


1. 介绍

吃豆人(Pac-Man)是1980年代风靡全球的街机游戏,其核心玩法简单却充满策略性。本文将基于Python的Pygame库,解析一个完整实现的吃豆人游戏代码。


代码包含以下核心功能:

  • 随机迷宫生成(每次游戏不同)

  • 幽灵AI追踪(A*算法实现)

  • 吃豆人移动与动画

  • 能量豆机制(幽灵恐惧状态)

  • 游戏状态管理(生命值、得分、胜负判定)

2. 代码结构概览

代码由6个核心类组成:

  1. MazeGenerator:迷宫生成

  2. GhostAI:幽灵路径算法

  3. Ghost:幽灵行为与渲染

  4. Pacman:玩家角色控制

  5. Game:游戏主逻辑

# 类关系示意图
Game 
├── MazeGenerator
├── Pacman
└── Ghost
    └── GhostAI

3. 核心功能解析

1. 迷宫生成算法(Prim算法)

实现原理

  • 初始化为全墙迷宫,随机选择一个起点(1,1)。

  • 使用“边界墙列表”逐步打通路径,每次随机选择一个边界墙,检查其对侧是否为墙。

  • 额外步骤:随机打通部分墙壁以增加环路,避免单一路径。

关键代码

       # 使用Prim算法生成多路径迷宫
        def prim_maze():
            # 从起点(1,1)开始
            frontier = []
            start = (1, 1)
            maze[start[1]][start[0]] = 0

            # 添加相邻墙到边界列表
            for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
                nx, ny = start[0] + dx, start[1] + dy
                if 0 < nx < width - 1 and 0 < ny < height - 1:
                    frontier.append((nx, ny, start[0], start[1]))

            while frontier:
                # 随机选择一个边界墙
                random.shuffle(frontier)
                x, y, px, py = frontier.pop()

                # 如果对面是墙,就打通
                opposite_x, opposite_y = px + (x - px) * 2, py + (y - py) * 2
                if 0 < opposite_x < width - 1 and 0 < opposite_y < height - 1:
                    if maze[opposite_y][opposite_x] == 1:
                        maze[y][x] = 0
                        maze[opposite_y][opposite_x] = 0

                        # 添加新的边界墙
                        for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
                            nx, ny = opposite_x + dx, opposite_y + dy
                            if 0 < nx < width - 1 and 0 < ny < height - 1 and maze[ny][nx] == 1:
                                frontier.append((nx, ny, opposite_x, opposite_y))

        prim_maze()

优化点

  • 确保起点和终点(1,1)和(width-2, height-2)始终连通。

  • 通过随机打通额外墙壁,迷宫复杂度提升80%。

2. 幽灵AI:A*路径追踪

算法核心

  • A*算法结合了Dijkstra的最短路径和启发式估计(曼哈顿距离)。

  • 幽灵每15帧重新计算路径,平衡性能与实时性。

代码实现

class GhostAI:
    @staticmethod
    def astar(maze, start, end):
        open_set = []
        heapq.heappush(open_set, (0, start))
        came_from = {}
        g_score = {start: 0}
        f_score = {start: GhostAI.heuristic(start, end)}

        while open_set:
            current = heapq.heappop(open_set)[1]

            if current == end:
                path = []
                while current in came_from:
                    path.append(current)
                    current = came_from[current]
                return path[::-1]

            for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
                neighbor = (current[0] + dx, current[1] + dy)
                if 0 <= neighbor[0] < len(maze[0]) and 0 <= neighbor[1] < len(maze):
                    if maze[neighbor[1]][neighbor[0]] == 1:
                        continue
                    tentative_g = g_score[current] + 1
                    if neighbor not in g_score or tentative_g < g_score[neighbor]:
                        came_from[neighbor] = current
                        g_score[neighbor] = tentative_g
                        f_score[neighbor] = tentative_g + GhostAI.heuristic(neighbor, end)
                        heapq.heappush(open_set, (f_score[neighbor], neighbor))
        return None

    @staticmethod
    def heuristic(a, b):
        return abs(a[0] - b[0]) + abs(a[1] - b[1])

恐惧模式

  • 吃能量豆后,幽灵变蓝并切换为随机逃跑模式:

if self.frightened:
    directions = [(0,1), (1,0), ...]
    random.shuffle(directions)  # 随机选择逃跑方向

3. 吃豆人控制与动画

移动机制

  • 方向键预输入:按下方向键时先检查路径合法性,再更新方向。

  • 边界穿越:移动到画面边缘时会从另一侧出现。

嘴巴动画

  • 通过角度变化实现开合效果:

# 每帧更新嘴巴角度
self.mouth_angle += self.mouth_direction * 5
if mouth_angle超过45度则反向

眼睛方向

  • 根据移动方向动态调整眼睛位置:

if direction == (-1,0):  # 向左
    eye_offset_x = -radius//4

4. 游戏逻辑与状态管理

碰撞检测

  • 圆形碰撞检测:计算吃豆人与幽灵的欧氏距离。

dist = sqrt((x1-x2)^2 + (y1-y2)^2)
if dist < 15:  # 发生碰撞

胜负条件

  • 胜利:吃光所有豆子和能量豆。

  • 失败:生命值归零。

重置游戏

  • 按R键调用reset_game(),重新生成迷宫并重置角色位置。

4. 完整代码

如下:
 

import pygame
import random
import heapq
from collections import deque

# 游戏常量
CELL_SIZE = 30
MAZE_WIDTH = 21  # 必须为奇数
MAZE_HEIGHT = 21  # 必须为奇数
WIDTH = MAZE_WIDTH * CELL_SIZE
HEIGHT = MAZE_HEIGHT * CELL_SIZE
PACMAN_SPEED = 3
GHOST_SPEED = 2

# 颜色定义
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
YELLOW = (255, 255, 0)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
PINK = (255, 184, 255)
CYAN = (0, 255, 255)
ORANGE = (255, 184, 82)


class MazeGenerator:
    @staticmethod
    def generate_maze(width, height):
        # 初始化全墙迷宫
        maze = [[1 for _ in range(width)] for _ in range(height)]

        # 使用Prim算法生成多路径迷宫
        def prim_maze():
            # 从起点(1,1)开始
            frontier = []
            start = (1, 1)
            maze[start[1]][start[0]] = 0

            # 添加相邻墙到边界列表
            for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
                nx, ny = start[0] + dx, start[1] + dy
                if 0 < nx < width - 1 and 0 < ny < height - 1:
                    frontier.append((nx, ny, start[0], start[1]))

            while frontier:
                # 随机选择一个边界墙
                random.shuffle(frontier)
                x, y, px, py = frontier.pop()

                # 如果对面是墙,就打通
                opposite_x, opposite_y = px + (x - px) * 2, py + (y - py) * 2
                if 0 < opposite_x < width - 1 and 0 < opposite_y < height - 1:
                    if maze[opposite_y][opposite_x] == 1:
                        maze[y][x] = 0
                        maze[opposite_y][opposite_x] = 0

                        # 添加新的边界墙
                        for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
                            nx, ny = opposite_x + dx, opposite_y + dy
                            if 0 < nx < width - 1 and 0 < ny < height - 1 and maze[ny][nx] == 1:
                                frontier.append((nx, ny, opposite_x, opposite_y))

        prim_maze()

        # 确保至少有多个环路
        for _ in range(width * height // 10):
            x, y = random.randint(1, width - 2), random.randint(1, height - 2)
            if maze[y][x] == 1:
                neighbors = 0
                for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
                    if maze[y + dy][x + dx] == 0:
                        neighbors += 1
                if neighbors >= 2:
                    maze[y][x] = 0

        # 确保起点和终点是通路
        maze[1][1] = 0
        maze[height - 2][width - 2] = 0

        return maze


class GhostAI:
    @staticmethod
    def astar(maze, start, end):
        open_set = []
        heapq.heappush(open_set, (0, start))
        came_from = {}
        g_score = {start: 0}
        f_score = {start: GhostAI.heuristic(start, end)}

        while open_set:
            current = heapq.heappop(open_set)[1]

            if current == end:
                path = []
                while current in came_from:
                    path.append(current)
                    current = came_from[current]
                return path[::-1]

            for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
                neighbor = (current[0] + dx, current[1] + dy)
                if 0 <= neighbor[0] < len(maze[0]) and 0 <= neighbor[1] < len(maze):
                    if maze[neighbor[1]][neighbor[0]] == 1:
                        continue
                    tentative_g = g_score[current] + 1
                    if neighbor not in g_score or tentative_g < g_score[neighbor]:
                        came_from[neighbor] = current
                        g_score[neighbor] = tentative_g
                        f_score[neighbor] = tentative_g + GhostAI.heuristic(neighbor, end)
                        heapq.heappush(open_set, (f_score[neighbor], neighbor))
        return None

    @staticmethod
    def heuristic(a, b):
        return abs(a[0] - b[0]) + abs(a[1] - b[1])


class Ghost:
    def __init__(self, x, y, color):
        self.x = x
        self.y = y
        self.color = color
        self.path = deque()
        self.speed = GHOST_SPEED
        self.frightened = False
        self.frightened_timer = 0

    def update(self, maze, pacman_pos):
        # 如果处于恐惧状态
        if self.frightened:
            self.frightened_timer -= 1
            if self.frightened_timer <= 0:
                self.frightened = False

        # 每15帧重新计算路径
        if pygame.time.get_ticks() % 15 == 0:
            start = (int(self.x // CELL_SIZE), int(self.y // CELL_SIZE))
            end = (int(pacman_pos[0] // CELL_SIZE), int(pacman_pos[1] // CELL_SIZE))

            if self.frightened:
                # 恐惧状态下随机逃跑
                directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
                random.shuffle(directions)
                for dx, dy in directions:
                    nx, ny = start[0] + dx, start[1] + dy
                    if 0 <= nx < len(maze[0]) and 0 <= ny < len(maze):
                        if maze[ny][nx] == 0:
                            end = (nx, ny)
                            break
            else:
                # 正常追踪
                end = (int(pacman_pos[0] // CELL_SIZE), int(pacman_pos[1] // CELL_SIZE))

            new_path = GhostAI.astar(maze, start, end)
            if new_path:
                self.path = deque(new_path)

        if self.path:
            target = self.path[0]
            target_x = target[0] * CELL_SIZE + CELL_SIZE // 2
            target_y = target[1] * CELL_SIZE + CELL_SIZE // 2

            # 平滑移动
            dx = target_x - self.x
            dy = target_y - self.y
            dist = (dx ** 2 + dy ** 2) ** 0.5

            if dist < 5:  # 接近目标点时切换到下一个路径点
                self.path.popleft()
            else:
                if dist != 0:
                    dx, dy = dx / dist * self.speed, dy / dist * self.speed
                self.x += dx
                self.y += dy

    def draw(self, screen):
        color = BLUE if self.frightened else self.color
        pygame.draw.circle(screen, color, (int(self.x), int(self.y)), CELL_SIZE // 2)

        # 画幽灵眼睛
        eye_size = CELL_SIZE // 6
        if not self.frightened:
            pygame.draw.circle(screen, WHITE, (int(self.x - CELL_SIZE // 5), int(self.y - CELL_SIZE // 8)), eye_size)
            pygame.draw.circle(screen, WHITE, (int(self.x + CELL_SIZE // 5), int(self.y - CELL_SIZE // 8)), eye_size)
            pygame.draw.circle(screen, BLACK, (int(self.x - CELL_SIZE // 5), int(self.y - CELL_SIZE // 8)),
                               eye_size // 2)
            pygame.draw.circle(screen, BLACK, (int(self.x + CELL_SIZE // 5), int(self.y - CELL_SIZE // 8)),
                               eye_size // 2)
        else:
            # 恐惧状态的眼睛
            pygame.draw.circle(screen, WHITE, (int(self.x - CELL_SIZE // 5), int(self.y - CELL_SIZE // 8)),
                               eye_size // 1.5)
            pygame.draw.circle(screen, WHITE, (int(self.x + CELL_SIZE // 5), int(self.y - CELL_SIZE // 8)),
                               eye_size // 1.5)
            pygame.draw.circle(screen, BLACK, (int(self.x - CELL_SIZE // 5), int(self.y - CELL_SIZE // 8)),
                               eye_size // 3)
            pygame.draw.circle(screen, BLACK, (int(self.x + CELL_SIZE // 5), int(self.y - CELL_SIZE // 8)),
                               eye_size // 3)


class Pacman:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.direction = (0, 0)
        self.next_direction = (0, 0)
        self.speed = PACMAN_SPEED
        self.mouth_angle = 0
        self.mouth_direction = 1
        self.radius = CELL_SIZE // 2 - 2

    def update(self, maze):
        # 尝试应用下一个方向
        if self.next_direction != (0, 0):
            next_x = self.x + self.next_direction[0] * self.speed
            next_y = self.y + self.next_direction[1] * self.speed
            cell_x = int(next_x // CELL_SIZE)
            cell_y = int(next_y // CELL_SIZE)

            if 0 <= cell_x < MAZE_WIDTH and 0 <= cell_y < MAZE_HEIGHT:
                if maze[cell_y][cell_x] == 0:
                    self.direction = self.next_direction

        # 移动
        new_x = self.x + self.direction[0] * self.speed
        new_y = self.y + self.direction[1] * self.speed
        cell_x = int(new_x // CELL_SIZE)
        cell_y = int(new_y // CELL_SIZE)

        # 检查是否碰到墙壁
        if 0 <= cell_x < MAZE_WIDTH and 0 <= cell_y < MAZE_HEIGHT:
            if maze[cell_y][cell_x] == 0:
                self.x = new_x
                self.y = new_y

        # 边界穿越
        if self.x < 0:
            self.x = WIDTH
        elif self.x > WIDTH:
            self.x = 0

        # 嘴巴动画
        self.mouth_angle += self.mouth_direction * 5
        if self.mouth_angle > 45 or self.mouth_angle < 0:
            self.mouth_direction *= -1

    def draw(self, screen):
        angle = 0
        if self.direction == (-1, 0):
            angle = 180
        elif self.direction == (0, -1):
            angle = 90
        elif self.direction == (0, 1):
            angle = 270

        # 绘制吃豆人(带嘴巴动画)
        center = (int(self.x), int(self.y))
        start_angle = (angle + self.mouth_angle) * 3.14 / 180
        end_angle = (angle + 360 - self.mouth_angle) * 3.14 / 180

        # 主体
        pygame.draw.circle(screen, YELLOW, center, self.radius)

        # 嘴巴
        if self.direction != (0, 0):
            pygame.draw.arc(screen, BLACK,
                            (self.x - self.radius, self.y - self.radius,
                             self.radius * 2, self.radius * 2),
                            start_angle, end_angle, self.radius)

        # 眼睛
        eye_offset_x = 0
        eye_offset_y = -self.radius // 3
        if self.direction == (-1, 0):
            eye_offset_x = -self.radius // 4
        elif self.direction == (1, 0):
            eye_offset_x = self.radius // 4
        elif self.direction == (0, -1):
            eye_offset_y = -self.radius // 2
        elif self.direction == (0, 1):
            eye_offset_y = 0

        pygame.draw.circle(screen, BLACK,
                           (int(self.x + eye_offset_x), int(self.y + eye_offset_y)),
                           self.radius // 5)


class Game:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
        pygame.display.set_caption("Pac-Man")
        self.clock = pygame.time.Clock()
        self.font = pygame.font.SysFont(None, 36)
        self.reset_game()

    def reset_game(self):
        self.maze = MazeGenerator.generate_maze(MAZE_WIDTH, MAZE_HEIGHT)

        # 吃豆人初始位置
        self.pacman = Pacman(CELL_SIZE + CELL_SIZE // 2, CELL_SIZE + CELL_SIZE // 2)

        # 创建多个幽灵
        self.ghosts = [
            Ghost(MAZE_WIDTH * CELL_SIZE - CELL_SIZE // 2 - CELL_SIZE,
                  MAZE_HEIGHT * CELL_SIZE - CELL_SIZE // 2 - CELL_SIZE, RED),
            Ghost(CELL_SIZE + CELL_SIZE // 2,
                  MAZE_HEIGHT * CELL_SIZE - CELL_SIZE // 2 - CELL_SIZE, PINK),
            Ghost(MAZE_WIDTH * CELL_SIZE - CELL_SIZE // 2 - CELL_SIZE,
                  CELL_SIZE + CELL_SIZE // 2, CYAN),
            Ghost(CELL_SIZE + CELL_SIZE // 2,
                  CELL_SIZE + CELL_SIZE // 2, ORANGE)
        ]

        # 生成豆子
        self.dots = []
        self.power_pellets = []
        for y in range(MAZE_HEIGHT):
            for x in range(MAZE_WIDTH):
                if self.maze[y][x] == 0:
                    if (x + y) % 7 == 0 and (x > 3 or y > 3) and (x < MAZE_WIDTH - 4 or y < MAZE_HEIGHT - 4):
                        self.power_pellets.append((x * CELL_SIZE + CELL_SIZE // 2,
                                                   y * CELL_SIZE + CELL_SIZE // 2))
                    else:
                        self.dots.append((x * CELL_SIZE + CELL_SIZE // 2,
                                          y * CELL_SIZE + CELL_SIZE // 2))

        self.score = 0
        self.lives = 3
        self.game_over = False
        self.win = False
        self.frightened_mode = False
        self.frightened_timer = 0

    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    return False
                elif event.key == pygame.K_r:
                    self.reset_game()
                    return True

                # 设置下一个方向
                if event.key == pygame.K_LEFT:
                    self.pacman.next_direction = (-1, 0)
                elif event.key == pygame.K_RIGHT:
                    self.pacman.next_direction = (1, 0)
                elif event.key == pygame.K_UP:
                    self.pacman.next_direction = (0, -1)
                elif event.key == pygame.K_DOWN:
                    self.pacman.next_direction = (0, 1)

        return True

    def update(self):
        if self.game_over or self.win:
            return

        # 更新吃豆人
        self.pacman.update(self.maze)

        # 更新幽灵
        for ghost in self.ghosts:
            ghost.update(self.maze, (self.pacman.x, self.pacman.y))

        # 吃豆子检测
        for dot in self.dots[:]:
            if ((self.pacman.x - dot[0]) ** 2 + (self.pacman.y - dot[1]) ** 2) ** 0.5 < CELL_SIZE // 3:
                self.dots.remove(dot)
                self.score += 10

        # 吃能量豆检测
        for pellet in self.power_pellets[:]:
            if ((self.pacman.x - pellet[0]) ** 2 + (self.pacman.y - pellet[1]) ** 2) ** 0.5 < CELL_SIZE // 3:
                self.power_pellets.remove(pellet)
                self.score += 50
                self.frightened_mode = True
                self.frightened_timer = 500  # 约8秒
                for ghost in self.ghosts:
                    ghost.frightened = True
                    ghost.frightened_timer = self.frightened_timer

        # 更新恐惧模式计时器
        if self.frightened_mode:
            self.frightened_timer -= 1
            if self.frightened_timer <= 0:
                self.frightened_mode = False
                for ghost in self.ghosts:
                    ghost.frightened = False

        # 幽灵碰撞检测
        for ghost in self.ghosts:
            dist = ((self.pacman.x - ghost.x) ** 2 + (self.pacman.y - ghost.y) ** 2) ** 0.5
            if dist < CELL_SIZE // 2:
                if ghost.frightened:
                    # 吃掉幽灵
                    ghost.x = MAZE_WIDTH * CELL_SIZE // 2
                    ghost.y = MAZE_HEIGHT * CELL_SIZE // 2
                    ghost.frightened = False
                    self.score += 200
                else:
                    # 被幽灵抓到
                    self.lives -= 1
                    if self.lives <= 0:
                        self.game_over = True
                    else:
                        # 重置位置
                        self.pacman.x = CELL_SIZE + CELL_SIZE // 2
                        self.pacman.y = CELL_SIZE + CELL_SIZE // 2
                        self.pacman.direction = (0, 0)
                        for g in self.ghosts:
                            g.x = random.choice([CELL_SIZE + CELL_SIZE // 2,
                                                 MAZE_WIDTH * CELL_SIZE - CELL_SIZE // 2 - CELL_SIZE])
                            g.y = random.choice([CELL_SIZE + CELL_SIZE // 2,
                                                 MAZE_HEIGHT * CELL_SIZE - CELL_SIZE // 2 - CELL_SIZE])
                            g.frightened = False
                    break

        # 胜利条件
        if not self.dots and not self.power_pellets:
            self.win = True

    def draw(self):
        self.screen.fill(BLACK)

        # 绘制迷宫
        for y in range(MAZE_HEIGHT):
            for x in range(MAZE_WIDTH):
                if self.maze[y][x] == 1:
                    pygame.draw.rect(self.screen, BLUE,
                                     (x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE))

        # 绘制豆子
        for dot in self.dots:
            pygame.draw.circle(self.screen, WHITE, dot, 3)

        # 绘制能量豆
        for pellet in self.power_pellets:
            pygame.draw.circle(self.screen, WHITE, pellet, 8)

        # 绘制幽灵
        for ghost in self.ghosts:
            ghost.draw(self.screen)

        # 绘制吃豆人
        self.pacman.draw(self.screen)

        # 绘制分数和生命
        score_text = self.font.render(f"Score: {self.score}", True, WHITE)
        lives_text = self.font.render(f"Lives: {self.lives}", True, WHITE)
        self.screen.blit(score_text, (10, 10))
        self.screen.blit(lives_text, (WIDTH - 120, 10))

        # 游戏结束或胜利提示
        if self.game_over:
            text = self.font.render("GAME OVER! Press R to restart", True, WHITE)
            self.screen.blit(text, (WIDTH // 2 - 180, HEIGHT // 2))
        elif self.win:
            text = self.font.render("YOU WIN! Press R to restart", True, WHITE)
            self.screen.blit(text, (WIDTH // 2 - 160, HEIGHT // 2))

        pygame.display.flip()

    def run(self):
        running = True
        while running:
            running = self.handle_events()
            self.update()
            self.draw()
            self.clock.tick(60)

        pygame.quit()


if __name__ == "__main__":
    game = Game()
    game.run()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

听风吹等浪起

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值