【Python小游戏】基于Pygame的递归回溯迷宫生成与BFS寻路实战:从算法原理到完整游戏架构的深度解析

引言:当古老的迷宫遇见现代算法

迷宫,这个承载着人类千年智慧的神秘结构,从克里特岛的米诺斯迷宫传说,到法国文艺复兴时期的皇家花园,再到现代心理学实验中的空间认知测试,始终以其独特的拓扑魅力吸引着探索者的目光。在数字时代的今天,迷宫不再仅仅是石块与绿植的物理堆砌,而是转化为比特世界中的抽象图结构,成为检验算法效率与程序架构设计的绝佳试验场。

本文将深入剖析一个基于Python Pygame框架开发的完整迷宫闯关游戏项目。这不仅仅是一个简单的游戏开发教程,更是一次关于递归回溯算法、广度优先搜索、游戏状态机架构以及人机交互设计的深度技术探索。我们将从生成第一个9×9的小型迷宫开始,逐步构建一个包含八级难度梯度、支持智能寻路辅助、具备完整UI体系的复杂游戏系统。在这个过程中,你会看到如何将纯粹的计算机科学理论转化为流畅的用户体验,如何用面向对象的设计思想管理游戏状态的生命周期,以及如何在性能与视觉效果之间寻找平衡点。

让我们打开代码编辑器,开始这场穿越数据结构与算法迷宫的旅程。


第一章:项目架构与核心机制概述

在深入探讨具体算法实现之前,有必要先建立对项目整体架构的认知。一个优秀的游戏项目不仅仅是功能的堆砌,更是模块间松耦合、高内聚的艺术体现。本项目的架构设计遵循了经典的游戏循环模式(Game Loop Pattern),但在状态管理和渲染管线方面进行了针对迷宫特性的深度优化。

游戏系统的整体蓝图

从宏观视角审视,整个游戏系统可以划分为四个核心子系统:迷宫生成引擎、渲染管线、状态管理器以及输入处理模块。这种分层架构确保了当用户从主菜单切换到第八关的37×37超大规模迷宫时,系统仍能保持稳定的帧率和响应速度。

迷宫生成引擎是项目的技术核心,它采用了递归回溯算法(Recursive Backtracking Algorithm)这一在迷宫生成领域被誉为"完美迷宫"制造机的经典方法。与简单的随机Prim算法或Kruskal算法相比,递归回溯生成的迷宫具有独特的" River "特性------即长廊与死胡同的有机分布,这种拓扑结构在人类玩家体验测试中往往被认为更具探索乐趣。引擎的关键在于其将迷宫抽象为二维数组的图论表示:值为1的单元格代表墙壁,值为0的通道则构成了可遍历的图节点集合。

渲染管线的设计充分考虑了迷宫规模的可变性。从第一关的9×9到第八关的37×37,单元格尺寸(cell_size)需要根据屏幕分辨率进行动态计算。项目中的自适应布局算法通过计算可用屏幕空间与迷宫维度的比值,确保在任何关卡下迷宫都能完整显示且保持合适的视觉比例。这种动态调整机制不仅涉及简单的除法运算,还需要考虑整数舍入对视觉对齐的影响,以及极小单元格(小于5像素)下的渲染优化策略------当迷宫规模过大时,系统会智能地禁用单元格边框绘制,以避免视觉噪声干扰玩家对路径的判断。

状态管理器采用了有限状态机(Finite State Machine, FSM)的设计模式。游戏在"menu"(菜单)、"level_select"(关卡选择)、"playing"(游玩中)、"won"(胜利)和"gameover"(超时)五个状态间流转。这种设计彻底解耦了不同场景下的逻辑处理:菜单状态下的鼠标点击事件不会误触发游戏角色的移动,而游戏结束后的输入处理也独立于主游戏循环。每个状态都拥有自己的事件处理器和渲染逻辑,通过game_state这一状态变量进行中央调度。

时间机制与难度曲线设计

游戏引入了时间压力机制------每关10分钟的限制。这不仅增加了游戏的紧张感,更从工程角度引入了实时时钟管理的需求。系统使用Python的time模块记录关卡开始时间戳,在每一帧更新时计算剩余时间。这种设计看似简单,实则涉及浮点数精度管理、系统时间回拨的容错处理(尽管在本机游戏中不常见),以及倒计时归零时的状态转换触发机制。

八级难度曲线的设计遵循了指数增长与线性增长相结合的策略。迷宫尺寸的计算公式cols = rows = 9 + (level - 1) * 4确保了从第一关到第八关,迷宫复杂度呈线性递增,但实际的解空间规模(即可能路径数量)却呈指数级爆炸。这种设计既保证了新手玩家能在前三关建立信心,又为资深玩家在后五关提供了足够的挑战性。值得注意的是第八关的特殊处理------代码中明确将第八关尺寸修正为37×37(而非公式计算的37×37,实际上公式计算结果也是37,但代码中单独处理确保了逻辑清晰),这种显式声明增强了代码的可读性和可维护性。

面向对象设计的实践哲学

MazeGame类作为整个系统的核心控制器,其设计体现了单一职责原则(Single Responsibility Principle)的灵活运用。类属性的初始化被严格区分为视觉配置(颜色、字体)、游戏参数(关卡数、时间限制)、运行时状态(玩家坐标、自动寻路标志)以及迷宫数据(二维数组)四大类别。这种分类不仅使代码结构清晰,更为后续的功能扩展预留了空间。

特别值得称道的是字体处理模块的设计。考虑到跨平台兼容性,系统实现了多级字体回退机制(Font Fallback Mechanism)。它首先尝试加载Windows系统的黑体、宋体、微软雅黑,随后检测macOS的苹方字体,最后尝试Linux的Noto Sans CJK系列。这种逐级回退的策略确保了无论用户身处何种操作系统环境,中文界面都能正确渲染,避免了常见的"方块字"问题。当所有系统字体都不可用时,系统会优雅地回退到Pygame默认字体,虽然此时中文显示会失效,但游戏的基本功能仍然保持可用,这种防御性编程(Defensive Programming)思想值得在跨平台开发中借鉴。


第二章:递归回溯算法的深度解构

迷宫生成算法是本项目的技术基石。在计算机科学领域,迷宫生成问题本质上是图论中生成树构造问题的可视化表现。我们所采用的递归回溯算法,实际上是在执行一种深度优先搜索(Depth-First Search, DFS)的随机化变体,它在遍历图的过程中"拆除"墙壁,最终构建出一棵覆盖所有可达节点的生成树。

算法原理的数学本质

从数学视角审视,一个完美的迷宫(Perfect Maze)在图论中有严格的定义:它是一个连通无环图(Connected Acyclic Graph),即树结构。这意味着迷宫中任意两点之间存在且仅存在一条唯一路径。递归回溯算法正是构建这种完美迷宫的高效方法,其时间复杂度为O(N),其中N为迷宫单元格数量,因为每个单元格恰好被访问一次。

算法的核心机制基于" carving "(雕刻)概念。初始状态下,迷宫被表示为一个全为墙壁(值为1)的二维网格。算法从起始点(通常是(1,1))出发,采用"凿墙通行"的策略:当移动到相邻的未访问单元格时,不仅将目标单元格标记为通道(值为0),还要将两者之间的墙壁单元格也标记为通道。这种"一步两格"的移动方式(代码中体现为dx, dy取值为±2而非±1)确保了墙壁的厚度始终为1个单元格,维持了迷宫的拓扑一致性。

随机性是赋予迷宫多样性的关键。在每一次移动决策时,算法使用random.shuffle(directions)对四个基本方向(上、下、左、右)进行随机排序。这种随机化决定了迷宫的"性格":如果优先选择垂直方向,迷宫会产生更多的纵向长廊;如果横向优先,则会形成横向为主的结构。在我们的实现中,完全随机的方向选择确保了每次生成的迷宫都具有独特的空间结构,这为游戏的重玩价值(Replay Value)提供了算法层面的保障。

递归实现与栈管理

递归回溯算法的优雅之处在于其自然地利用了编程语言的调用栈(Call Stack)来管理访问路径。在_recursive_backtrack方法的实现中,每一次递归调用都隐式地将当前坐标压入系统栈,当某个单元格的所有相邻方向都被墙壁或边界阻挡时(即进入死胡同),函数开始返回,栈帧弹出,系统回溯到上一个还有未探索方向的节点。

这种隐式栈管理虽然代码简洁,但在处理超大规模迷宫时存在栈溢出的风险。Python默认的递归深度限制(通常为1000)理论上可以支持约500×500规模的迷宫生成(因为递归深度与迷宫最长路径相关,而非总单元格数),但对于37×37的第八关来说绰绰有余。然而,如果未来需要扩展到100×100以上的超大规模迷宫,就需要将递归实现改写为显式栈管理的迭代实现,通过手动维护一个stack列表来模拟递归过程,从而规避Python的递归深度限制。

墙壁处理与边界条件

在迷宫生成的具体实现中,墙壁数据的处理体现了工程细节的重要性。代码通过wall_x = x + dx // 2wall_y = y + dy // 2计算出当前位置与目标位置之间的墙壁坐标,并将其置为0(通道)。这种整数除法技巧确保了无论dx是+2还是-2,计算结果都正确指向中间位置。

边界条件的处理采用了"围栏"(Fence)模式:迷宫的外围始终保留一圈墙壁,即坐标(0, y)、(cols-1, y)、(x, 0)、(x, rows-1)永远为1。这种设计不仅符合真实迷宫的物理直觉(不可能走出边界),也简化了路径查找算法的边界检查------只需要检查数组越界,而不需要额外的逻辑判断。在初始化时,通过self.maze = [[1 for _ in range(self.cols)] for _ in range(self.rows)]创建的全1矩阵自然形成了这种围栏结构。

随机性与确定性的平衡

虽然算法依赖于随机数生成器,但良好的迷宫生成需要避免某些极端情况。例如,纯粹的随机可能导致迷宫过于简单(大量短走廊)或过于复杂(过多的死胡同)。在我们的实现中,递归回溯算法天然地倾向于生成具有长走廊(Long Corridor)特征的迷宫,这是因为DFS策略会尽可能地深入探索一个方向直到碰壁才回溯。这种"深度优先"特性与人类探索迷宫的行为模式形成有趣的对照:玩家往往也会选择一条路走到底,直到发现死胡同时才返回。

为了验证生成算法的有效性,可以在generate_maze方法后添加连通性检测逻辑,使用并查集(Union-Find)或简单的Flood Fill算法验证从起点到终点的可达性。虽然理论上完美迷宫生成算法保证连通性,但在实际开发中,由于浮点数精度问题或多线程环境下的随机数竞争(尽管本项目未使用多线程),添加这种断言(Assertion)可以增强系统的健壮性。


第三章:BFS寻路算法与自动导航系统

当玩家在第8关的37×37巨大迷宫中迷失方向时,自动寻路功能不仅是游戏辅助工具,更是展示图搜索算法魅力的窗口。本项目实现的寻路系统基于广度优先搜索(Breadth-First Search, BFS),这是无权图中最短路径问题的经典解法。

图搜索的理论基础

在将迷宫抽象为图结构时,每个通道单元格(值为0)都是一个节点,而相邻(上下左右)的通道单元格之间则存在边。由于所有边的权重相等(移动一步的代价相同),BFS能够保证找到的路径不仅是可达的,而且是步数最少的最短路径。这与A算法形成对比------A通过启发函数(Heuristic Function)加速搜索,但在简单网格迷宫中,BFS的简洁性和完备性往往更具教学价值。

BFS的核心思想是"逐层扩散"。想象在迷宫中滴入一滴墨水,它首先染色的起始点周围的四个邻居,然后是邻居的邻居,依此类推,直到墨水抵达终点。在代码实现中,这种扩散过程通过collections.deque(双端队列)高效管理。与列表的pop(0)操作具有O(n)时间复杂度不同,dequepopleft()操作是O(1),这对于处理大规模迷宫的路径搜索至关重要。

路径重建与状态追踪

单纯的BFS只能判断可达性,要获得具体路径需要额外的追踪机制。在find_path_to_end方法的实现中,队列存储的不仅是当前坐标(x, y),还包括到达该坐标所经过的路径path。这种设计在概念上清晰易懂,但在内存使用上存在优化空间------对于长路径,频繁的列表复制(path + [(new_x, new_y)])会产生大量临时对象。

更高效的实现方式是维护一个visited字典,记录每个节点的前驱节点(Parent Pointer)。当搜索到达终点时,通过反向追踪前驱节点链即可重建路径。这种优化将空间复杂度从O(V×L)(V为访问节点数,L为路径长度)降低到O(V),对于37×37=1369个节点的迷宫而言,这种优化可能微不足道,但对于更复杂的图结构,这种差异将变得显著。

自动移动的状态机集成

找到路径只是第一步,如何让玩家角色平滑地沿路径移动则涉及游戏循环的时序控制。项目中的自动移动系统实现了精妙的状态管理:当玩家按下空格键触发toggle_auto_move时,系统首先调用find_path_to_end计算路径,然后将is_auto_moving标志置为True,启动自动移动状态。

自动移动的时序控制通过时间戳比较实现,而非简单的帧计数。self.last_auto_time记录了上一次移动的时间,self.auto_speed(设置为0.05秒)定义了移动间隔。这种基于时间的动画(Time-based Animation)确保了无论游戏运行在60FPS还是30FPS,角色的移动速度都保持一致。在run方法的主循环中,每次迭代检查当前时间与last_auto_time的差值,满足条件则移动到auto_path中的下一个坐标点。

这种设计还支持随时中断------如果玩家在自动移动过程中按下方向键,move_player方法会检查is_auto_moving状态并立即返回,同时玩家操作会将is_auto_moving重置为False(虽然代码中move_player直接返回,但更好的做法是在其中添加self.is_auto_moving = False以确保状态一致性)。这种人机协作模式体现了良好的用户体验设计:AI辅助但不强制,玩家始终保有控制权。

寻路失败的处理策略

在健壮性设计中,考虑算法失败的情况至关重要。虽然在本项目的完美迷宫中,从起点到终点必然存在路径,但在代码的find_path_to_end方法中,仍然处理了队列为空(即无路可达)的情况。当BFS耗尽所有可能节点仍未找到终点时,方法将self.auto_path置为空列表。在toggle_auto_move中,通过检查if self.auto_path来避免在空路径上启动自动移动,并打印调试信息。这种防御性编程确保了即使在迷宫生成逻辑出现异常(如意外堵死了终点)时,系统也不会崩溃或进入死循环。


第四章:游戏引擎架构与事件系统

Pygame作为Python生态中最成熟的2D游戏开发框架,提供了底层的图形渲染、音频处理和输入管理功能。然而,如何在这些基础积木之上构建高可维护性的游戏架构,考验着开发者的软件工程设计能力。本项目展示了一种清晰的分层架构模式,将引擎接口与游戏逻辑有效分离。

主循环与帧率控制

游戏的主循环(Main Loop)位于run方法中,这是所有游戏活动的中央调度器。典型的游戏循环遵循"处理输入→更新状态→渲染画面"的三阶段模式。在本项目中,由于采用了事件驱动的状态更新(而非固定时间步长更新),循环结构稍有变化:输入处理分散在各个状态的特定方法中,而状态更新则体现在remaining_time的计算和自动移动的时序检查上。

帧率控制通过pygame.time.Clock实现,self.clock.tick(self.fps)确保游戏以每秒60帧的速度运行。这不仅控制了CPU使用率,也为动画提供了稳定的时间基准。值得注意的是,在菜单和胜利界面等静态场景中,代码使用了self.clock.tick(30)将帧率降低至30FPS,这种优化在不牺牲用户体验的前提下减少了系统资源消耗,体现了"按需分配"的性能优化思想。

事件处理的分发机制

Pygame的事件系统是观察者模式(Observer Pattern)的具体实现。系统通过pygame.event.get()获取事件队列中的所有事件,然后根据类型(QUIT、MOUSEBUTTONDOWN、KEYDOWN等)进行分发。在本项目中,事件处理呈现出明显的状态依赖性:在游戏进行中(playing状态),方向键控制角色移动;在菜单状态,同样的按键可能被忽略或具有不同功能。

这种基于状态的事件分发避免了复杂的条件判断嵌套。例如,如果在游戏进行中处理菜单逻辑,代码将充斥着if self.game_state == 'menu'之类的检查。而通过将事件处理逻辑封装在show_menuwin_level等状态特定的方法中,每个方法只需关注自身状态下的输入响应,大大提升了代码的可读性和可测试性。

键盘事件的处理展示了输入映射(Input Mapping)的灵活设计。项目同时支持方向键和WASD键位,这是通过分别检查pygame.K_UPpygame.K_w实现的。这种双重映射迎合了不同玩家的操作习惯:习惯主机游戏手柄布局的玩家可能偏好WASD,而习惯键盘方向键的玩家则可以使用箭头。更进一步的扩展可以实现完全自定义的键位映射,将按键编码与动作逻辑通过配置文件解耦。

渲染管线的层次结构

游戏的视觉呈现采用分层渲染策略。最底层是screen.fill(self.WHITE)清屏操作,确保上一帧的像素不会残留。随后是迷宫网格的绘制,通过双层循环遍历self.maze二维数组,根据单元格值选择绘制黑色墙壁或白色通道。为了优化性能,当cell_size小于5像素时,代码智能地跳过了边框绘制(pygame.draw.rect的最后一个参数),这种细节优化在37×37的大规模迷宫中尤为重要,减少了约50%的绘制调用。

玩家和终点的绘制采用了圆形而非矩形,这在视觉上更具辨识度。通过max(3, self.cell_size // 3)动态计算半径,确保了在不同缩放级别下图形的可识别性。信息栏(Info Bar)的绘制固定在屏幕顶部,采用半透明或纯色背景与游戏区域区分,这种HUD(Head-Up Display)设计符合现代游戏界面规范。

状态转换与数据持久化

游戏状态之间的转换需要谨慎管理资源。例如,从菜单进入游戏时,initialize_level方法被调用,它负责重置计时器、生成新迷宫、初始化玩家位置。这种集中式的初始化确保了状态转换的原子性------不会出现玩家位置是新的但迷宫是旧的这种不一致状态。

然而,当前的架构在数据持久化方面仍有改进空间。玩家的最佳通关时间、已解锁的关卡等数据存储在内存中,游戏重启后丢失。在实际生产环境中,应该引入JSON或SQLite进行本地数据持久化。此外,关卡间的连续性也可以增强:例如,允许玩家从第5关继续而不是每次从第1关开始,这需要将current_level等状态保存到磁盘。


第五章:用户界面设计与交互体验

优秀的游戏不仅需要稳固的技术底层,更需要直观美观的用户界面(UI)和流畅的用户体验(UX)。本项目在UI实现上充分考虑了中文显示的特殊性、不同屏幕尺寸的适应性以及交互反馈的即时性。

字体渲染的跨平台挑战

中文字体渲染是许多Python开发者面临的痛点。Pygame默认的Bitmap字体不支持中文,而系统字体路径在不同操作系统间差异巨大。本项目的解决方案是建立了一个字体候选列表,涵盖Windows、macOS和Linux的常见中文字体路径。通过os.path.exists检查文件存在性,结合try-except块处理字体加载异常,实现了鲁棒的字体回退机制。

这种设计的巧妙之处在于优先级排序:黑体(SimHei)作为首选,因其笔画清晰适合游戏标题;宋体(SimSun)作为备选;最后是泛用性强的Noto Sans CJK。当所有中文字体都不可用时,系统回退到Pygame默认字体,虽然此时中文显示为乱码,但游戏逻辑仍然可运行,这种"优雅降级"(Graceful Degradation)策略确保了核心功能不因次要问题而崩溃。

字体大小的层次结构也经过精心设计:font_title(48pt)用于大标题,font_large(32pt)用于副标题,font_normal(20pt)用于按钮和说明文字,font_small(16pt)用于提示信息。这种四级字体体系建立了清晰的视觉层次(Visual Hierarchy),引导玩家自然地关注到最重要的信息。

菜单系统的状态管理

主菜单和关卡选择界面采用了不同的布局策略。主菜单使用垂直流式布局,标题在上,规则说明居中,操作按钮在下,符合从上至下的阅读习惯。关卡选择则采用双列网格布局,将8个关卡分为4行2列,充分利用了水平空间,减少了垂直滚动需求。

按钮的交互反馈体现在视觉状态变化上。虽然代码中没有实现鼠标悬停(Hover)效果,但选中的关卡按钮会通过黄色高亮(self.YELLOW填充)和灰色边框与普通按钮区分。这种视觉反馈明确了用户的当前选择。在更完善的实现中,可以添加鼠标悬停时的颜色变化、点击时的缩放动画,以及禁用状态(如未解锁关卡)的灰度显示,进一步提升交互质感。

响应式布局与自适应计算

考虑到迷宫尺寸的变化范围极大(从9×9到37×37),固定单元格大小显然不可行。项目中的自适应布局算法通过计算可用空间与迷宫维度的比值,动态确定cell_size。具体而言,available_height减去了150像素为信息栏预留空间,available_width减去40像素作为边距。max_cell_size_by_heightmax_cell_size_by_width分别计算高度和宽度限制下的最大单元格尺寸,最终取较小值确保迷宫不会溢出屏幕。

这种计算还需要处理整数舍入问题。cell_size被强制转换为整数(通过整数除法//),这可能导致迷宫总宽度小于屏幕宽度,产生居中的黑边效果。offset_xoffset_y的计算正是为了处理这种居中对齐,通过(self.screen_width - maze_width) // 2计算左侧留白,使迷宫在视觉上始终处于屏幕中心。

游戏内HUD与信息架构

游戏进行中的界面(HUD)设计遵循"最小干扰"原则。顶部90像素高度的信息栏采用浅灰色背景,与白色的迷宫区域形成对比但不突兀。信息显示采用左对齐布局:左上角是关卡信息("第X关"),左下角是倒计时,右上角显示自动寻路状态,右下角是操作提示。

倒计时的时间颜色变化是情感化设计的体现:当剩余时间大于60秒时显示黑色,小于60秒时变为红色。这种颜色编码利用了人类对红色的本能警觉反应,无声地提醒玩家时间紧迫。类似的,自动寻路状态的蓝色文字不仅提供了功能反馈,也解释了为什么此时方向键可能"失效"(实际上代码中自动移动时手动移动被禁用,但更好的UX可能是允许手动移动打断自动寻路,而非完全禁用)。


第六章:关卡设计与游戏平衡

游戏设计不仅是技术实现,更是心理学与数学的结合。八关卡的难度曲线设计、时间限制的心理压力调控,以及自动寻路功能的定位,共同构成了本项目的游戏机制设计。

难度曲线的数学建模

难度设计遵循了"易→难"的渐进式上升,但具体的参数选择蕴含深意。迷宫尺寸从9×9开始,每关增加4个单位,到第八关达到37×37。这种线性增长在感官上似乎温和,但解空间的增长却是指数级的。对于完美迷宫,从入口到出口的最短路径长度期望与迷宫尺寸成正比,但可能的错误路径分支数量却随尺寸呈超线性增长。

具体计算,9×9迷宫有81个单元格,37×37迷宫有1369个单元格,后者是前者的16.9倍。然而,由于分支因子的存在,探索复杂度的增长远超16.9倍。这种设计确保了前几关(9×9、13×13、17×17)作为教学关,玩家可以在2-3分钟内轻松完成,建立掌控感;而后几关(29×29、33×33、37×37)则提供了真正的挑战,即使使用自动寻路,仅仅是观看角色走完路径也需要可观的时间。

时间限制设置为每关10分钟(600秒),这个数值经过精心权衡。对于熟练玩家,使用手动操作通关37×37迷宫通常需要5-8分钟,留出2分钟的安全边际;对于新手,即使多次试错,10分钟也足够完成探索。这种"宽限期"设计避免了因时间过紧导致的挫败感,同时保留了紧张感。

终点选择的算法逻辑

迷宫生成后,终点的位置并非随意设定。find_farthest_endpoint方法使用BFS从起点出发,计算到达所有其他可达点的距离,选择距离最远的点作为终点。这种设计确保了迷宫的"有效长度"最大化,避免了起点和终点过近导致的"秒通关"情况。

从图论角度,这种方法寻找的是生成树的直径(Diameter)的一个端点。虽然由于迷宫生成时的随机性,这不一定严格是图的直径,但实践中通常能找到足够远的点。这种算法也增加了游戏的可预测性:无论迷宫如何随机生成,玩家都知道终点总是在"最深"的地方,这种元知识(Meta-knowledge)成为玩家制定探索策略的基础。

辅助功能与游戏性的平衡

自动寻路功能的引入是一个设计亮点,它解决了传统迷宫游戏的一个痛点:在超大规模迷宫中,纯粹的手动探索可能变成枯燥的机械劳动。然而,如何避免自动寻路破坏游戏乐趣,需要细致的平衡。

本项目的解决方案是"辅助而非替代"。自动寻路启动后,角色以每0.05秒一步的速度移动,这个速度足够快以节省时间,又足够慢让玩家能够观察路径、理解迷宫结构。更重要的是,玩家可以随时中断自动寻路(通过方向键或特定按键,虽然当前代码中move_player在自动移动时直接返回,建议改进为允许中断),保持了对游戏的控制感。

这种设计体现了"无障碍设计"(Accessibility Design)的理念:它帮助那些空间认知能力较弱的玩家完成游戏,又不剥夺硬核玩家的手动挑战乐趣。在实际游戏中,玩家可能会采用混合策略:前几关手动探索,后几关先手动尝试,遇到困难时启用自动寻路学习正确路径,然后在后续重玩中挑战手动通关。


第七章:代码细节与工程实践

深入代码的微观层面,我们可以发现许多体现软件工程最佳实践的细节。这些细节虽然不直接影响游戏玩法,但对于代码的可维护性、可扩展性和健壮性至关重要。

数据结构的选择与优化

迷宫的存储采用了二维列表(List of Lists)结构self.maze[y][x]。这种选择在Python中是合理且高效的,因为列表的索引访问是O(1)操作。然而,对于超大规模迷宫(如1000×1000),可以考虑使用array模块或numpy数组来减少内存占用,特别是当只需要存储0/1二值数据时,可以使用位运算进一步优化。

路径存储在auto_path列表中,使用元组(x, y)表示坐标。虽然在小规模数据下影响不大,但在频繁的路径计算中,元组的创建和销毁有一定开销。对于性能敏感的场景,可以考虑将坐标编码为单个整数(如x * rows + y),或使用简单的命名元组(Named Tuple)提高可读性。

collections.deque的使用在BFS算法中至关重要。与普通列表相比,deque在两端添加和删除元素都是O(1),而列表在头部插入/删除是O(n)。在寻路算法中,这直接影响到处理大规模迷宫时的响应速度。

防御性编程与边界检查

代码中处处可见防御性编程的痕迹。在move_player方法中,移动前检查了新坐标是否在边界内(0 <= new_x < self.cols)且不是墙壁(self.maze[new_y][new_x] == 0)。这种双重检查防止了数组越界异常(IndexError)和非法移动。

在字体加载的get_font方法中,使用了try-except块捕获所有异常,并配合os.path.exists进行前置检查。这种"检查-尝试"的双重保险确保了即使字体文件损坏或权限不足,程序也能优雅降级。

时间计算中,max(0, self.remaining_time)的使用防止了倒计时出现负数显示,这种边界处理虽然微小,却体现了对用户体验的细致关注。

命名规范与代码可读性

项目遵循了Python的PEP 8命名规范:类名使用大驼峰(MazeGame),常量使用全大写(BLACK, WHITE),实例变量和方法使用小写下划线(generate_maze, cell_size)。这种一致性使得代码易于阅读和维护。

方法命名具有描述性,如find_farthest_endpoint明确表达了方法的功能,而toggle_auto_move中的"toggle"一词准确描述了状态的切换特性。变量名如is_auto_moving使用"is_"前缀明确表示布尔类型,auto_stepauto_path的"auto_"前缀表明它们与自动寻路系统的关联。

潜在的代码重构点

虽然当前代码结构良好,但仍存在重构空间。例如,draw_mazedraw_playerdraw_endpointdraw_info_bar四个绘制方法都直接操作self.screen,可以考虑引入一个专门的Renderer类,将绘制逻辑与游戏状态逻辑分离。这种分离使得未来更换渲染后端(如使用OpenGL加速或导出为图像序列)变得容易。

另外,run方法中的主循环略显冗长,特别是自动移动和超时检查的逻辑。可以提取一个update方法,封装所有与帧无关的状态更新逻辑,使主循环更清晰。

事件处理目前采用长串的if-elif结构,对于更多按键支持的情况,可以改用字典映射(Dispatch Table),将按键常量映射到处理方法,提高扩展性。


第八章:扩展思路与未来展望

当前的项目已经是一个功能完整的迷宫游戏,但技术的探索永无止境。基于现有架构,可以向多个方向扩展,融入更先进的算法和技术。

AI算法的深度集成

当前实现的自动寻路只是AI的雏形。可以引入更高级的算法,如A*(A-Star)算法,通过曼哈顿距离(Manhattan Distance)或欧几里得距离作为启发函数,大幅减少搜索节点数,在超大规模迷宫中实现几乎瞬时的路径查找。对于追求挑战性的玩家,可以实现"幽灵AI"------模拟其他探索者在迷宫中移动,与玩家竞争到达终点。

机器学习也可以融入项目。使用强化学习(Reinforcement Learning)训练一个AI代理玩迷宫游戏,通过Q-Learning或Deep Q-Network(DQN)让AI学习最优探索策略。这种AI不仅可以作为对手,还可以分析迷宫的难度------通过统计AI学习该迷宫所需的训练步数,可以量化评估迷宫的复杂度,实现动态难度调整(Dynamic Difficulty Adjustment)。

3D化与视觉效果升级

虽然Pygame本质上是2D引擎,但可以通过射线投射(Ray Casting)技术实现伪3D效果,类似于经典的《Wolfenstein 3D》。将俯视视角转变为第一人称视角,玩家可以看到"眼前"的墙壁,这将彻底改变游戏的空间感知和紧张感。

在保持2D视角的前提下,可以添加粒子效果(墙壁拆除时的尘土、移动时的脚印)、屏幕震动(撞墙时的反馈)和光影效果(视野受限,只有玩家周围一定范围内可见,其余为迷雾)。这些视觉效果不仅提升美观度,还能增加游戏机制------例如受限视野迫使玩家依赖记忆或地图。

关卡编辑器与程序化生成

当前的关卡完全由算法生成,缺乏人工设计的精妙。可以开发一个可视化关卡编辑器,允许设计师手动放置墙壁、陷阱、传送门等元素,保存为JSON或自定义二进制格式。这种混合生成方式(Procedural + Handmade)结合了两者的优点:算法的无限 variety 和人工设计的精巧机关。

更进一步的程序化生成可以引入"主题"概念:森林主题(绿色调,树状分支多的迷宫)、冰川主题(蓝色调,长走廊多的迷宫)、废墟主题(破碎的墙壁,多循环路径)。通过调整递归回溯算法的参数(如方向选择的权重、回溯的概率),可以生成具有不同"性格"的迷宫。

网络功能与社交元素

将单机游戏扩展为多人在线协作或对战模式。协作模式下,多名玩家在同一迷宫中探索,可以分头寻找路径,通过语音或文字交流信息。对战模式则可以设置为"竞速到达终点"或"捉迷藏"(一方扮演迷宫守卫,另一方逃避追捕)。

引入排行榜系统,记录全球玩家的最快通关时间,使用WebSocket实现实时排名更新。这种社交竞争机制大大延长了游戏的生命周期。

跨平台与移动端适配

Pygame支持打包为Windows、macOS和Linux的可执行文件。通过Kivy或BeeWare等框架,甚至可以将代码移植到Android和iOS平台。移动端的触控操作需要重新设计交互方式:虚拟方向键、滑动控制或点击目标点自动寻路。

在Web端,使用Pygame的WebAssembly移植版(如Pygame-web)可以让游戏直接在浏览器中运行,无需下载安装,降低体验门槛。


结语:在代码的迷宫中寻找出口

回顾整个项目的构建过程,我们不仅仅是在编写一个游戏,更是在探索计算机科学的多个核心领域:图论中的生成树与搜索算法、软件工程中的架构设计、人机交互中的用户体验优化。迷宫,这个看似简单的结构,实则蕴含了深刻的数学美感和工程挑战。

递归回溯算法教会我们,有时候需要勇敢地深入未知(递归深入),也要懂得适时回头(回溯),这种思想不仅适用于算法,也适用于软件开发中的迭代过程。BFS寻路提醒我们,系统地逐层探索往往比盲目的深度钻探更能找到最优解。而游戏架构的设计则展示了如何将复杂系统分解为可管理的模块,通过清晰的接口实现协作。

当我们运行这个游戏,看着绿色的小圆点在黑色墙壁构成的迷宫中穿行,我们看到的不仅是像素的移动,更是抽象思维的具象化。每一面墙壁的拆除都是概率与随机性的舞蹈,每一次成功的寻路都是图论定理的实践验证。

技术的终极价值在于创造体验。希望这个项目不仅能作为学习Pygame和算法的教材,更能激发读者对编程的热情------那种在复杂问题中寻找优雅解决方案,在逻辑迷宫中找到出口的喜悦。毕竟,生活本身就是一个巨大的迷宫,而编程思维,或许就是我们手中最可靠的自动寻路系统。

愿你在代码的世界中,永远保持探索的勇气和回溯的智慧。

完整代码如下:

复制代码
import pygame
import random
import time
from collections import deque
import sys
import os


class MazeGame:
    def __init__(self):
        pygame.init()

        # 屏幕设置 - 减小窗口高度以适应普通屏幕
        self.screen_width = 1000
        self.screen_height = 700
        self.screen = pygame.display.set_mode((self.screen_width, self.screen_height))
        pygame.display.set_caption("迷宫闯关游戏")

        # 颜色定义
        self.BLACK = (0, 0, 0)
        self.WHITE = (255, 255, 255)
        self.GREEN = (0, 128, 0)
        self.RED = (255, 0, 0)
        self.DARK_GREEN = (0, 100, 0)
        self.DARK_RED = (139, 0, 0)
        self.LIGHT_GRAY = (200, 200, 200)
        self.BLUE = (0, 0, 255)
        self.DARK_BLUE = (0, 0, 139)
        self.YELLOW = (255, 255, 0)
        self.GRAY = (128, 128, 128)

        # 字体设置 - 支持中文
        self.font_title = self.get_font(48)
        self.font_large = self.get_font(32)
        self.font_normal = self.get_font(20)
        self.font_small = self.get_font(16)

        # 游戏参数
        self.total_levels = 8
        self.current_level = 1
        self.time_limit = 10 * 60  # 秒
        self.start_time = None
        self.remaining_time = self.time_limit

        # 游戏状态
        self.game_state = "menu"  # menu, level_select, playing, won, gameover
        self.is_auto_moving = False
        self.auto_path = []
        self.auto_step = 0
        self.auto_speed = 0.05  # 加快自动寻路速度
        self.last_auto_time = 0

        # 初始化迷宫相关属性(防止未定义错误)
        self.cols = 9
        self.rows = 9
        self.cell_size = 20
        self.maze = []
        self.player_x = 1
        self.player_y = 1
        self.end_x = 7
        self.end_y = 7
        self.offset_x = 0
        self.offset_y = 0

        # 时钟
        self.clock = pygame.time.Clock()
        self.fps = 60

    def get_font(self, size):
        """获取支持中文的字体"""
        # 尝试多个常见的Windows中文字体路径
        font_paths = [
            "C:\\Windows\\Fonts\\simhei.ttf",  # 黑体
            "C:\\Windows\\Fonts\\simsun.ttc",  # 宋体
            "C:\\Windows\\Fonts\\msyh.ttc",  # 微软雅黑
            "/System/Library/Fonts/PingFang.ttc",  # macOS
            "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",  # Linux
        ]

        # 尝试使用系统字体
        for font_path in font_paths:
            if os.path.exists(font_path):
                try:
                    return pygame.font.Font(font_path, size)
                except:
                    pass

        # 如果都失败,使用pygame默认字体
        return pygame.font.Font(None, size)

    def get_level_params(self, level):
        """获取指定关卡的参数"""
        if level == 8:
            cols = rows = 37
        else:
            cols = rows = 9 + (level - 1) * 4

        # 格子大小自适应:保证迷宫显示在合适区域内(预留信息栏空间)
        available_height = self.screen_height - 150  # 预留信息栏和边距
        available_width = self.screen_width - 40
        max_cell_size_by_height = available_height // cols
        max_cell_size_by_width = available_width // cols
        cell_size = max(3, min(max_cell_size_by_height, max_cell_size_by_width))

        return cols, rows, cell_size

    def show_menu(self):
        """显示主菜单"""
        self.game_state = "menu"

        while self.game_state == "menu":
            self.screen.fill(self.WHITE)

            # 标题
            title = self.font_title.render("迷宫闯关游戏", True, self.DARK_BLUE)
            title_rect = title.get_rect(center=(self.screen_width // 2, 40))
            self.screen.blit(title, title_rect)

            # 规则说明
            y_pos = 100

            # 游戏规则
            rule_title = self.font_large.render("游戏规则:", True, self.DARK_GREEN)
            self.screen.blit(rule_title, (80, y_pos))
            y_pos += 35

            rules = [
                "● 共8关,难度逐级递增",
                "● 每关时间限制:10分钟",
                "● 第1关:9×9  →  第8关:35×35",
                "● 每关参数增大:+4",
            ]

            for rule in rules:
                rule_text = self.font_normal.render(rule, True, self.BLACK)
                self.screen.blit(rule_text, (100, y_pos))
                y_pos += 28

            y_pos += 15

            # 控制方法
            control_title = self.font_large.render("控制方法:", True, self.DARK_GREEN)
            self.screen.blit(control_title, (80, y_pos))
            y_pos += 35

            controls = [
                "● 方向键或 WASD 移动",
                "● 空格键或回车键 自动寻路到终点",
                "● R 键 重新开始当前关卡",
                "● 点击\"选择关卡\"选择开始关卡",
            ]

            for control in controls:
                control_text = self.font_normal.render(control, True, self.BLACK)
                self.screen.blit(control_text, (100, y_pos))
                y_pos += 28

            y_pos += 25

            # 按钮
            button_width = 180
            button_height = 45
            button_x = (self.screen_width - button_width) // 2
            button_y = y_pos

            # 选择关卡按钮
            button_rect1 = pygame.Rect(button_x - 200, button_y, button_width, button_height)
            pygame.draw.rect(self.screen, self.BLUE, button_rect1)
            pygame.draw.rect(self.screen, self.DARK_BLUE, button_rect1, 3)
            button_text1 = self.font_normal.render("选择关卡", True, self.WHITE)
            text_rect1 = button_text1.get_rect(center=button_rect1.center)
            self.screen.blit(button_text1, text_rect1)

            # 开始游戏按钮
            button_rect2 = pygame.Rect(button_x + 20, button_y, button_width, button_height)
            pygame.draw.rect(self.screen, self.GREEN, button_rect2)
            pygame.draw.rect(self.screen, self.DARK_GREEN, button_rect2, 3)
            button_text2 = self.font_normal.render("开始游戏", True, self.WHITE)
            text_rect2 = button_text2.get_rect(center=button_rect2.center)
            self.screen.blit(button_text2, text_rect2)

            pygame.display.flip()

            # 事件处理
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    return False
                elif event.type == pygame.MOUSEBUTTONDOWN:
                    if button_rect1.collidepoint(event.pos):
                        if self.show_level_selection():
                            self.game_state = "playing"
                            return True
                        return True
                    elif button_rect2.collidepoint(event.pos):
                        self.game_state = "playing"
                        return True
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_RETURN:
                        self.game_state = "playing"
                        return True

        return True

    def show_level_selection(self):
        """显示关卡选择界面"""
        selecting = True
        level_var = self.current_level

        while selecting:
            self.screen.fill(self.WHITE)

            # 标题
            title = self.font_large.render("选择开始关卡", True, self.DARK_BLUE)
            title_rect = title.get_rect(center=(self.screen_width // 2, 25))
            self.screen.blit(title, title_rect)

            y_pos = 70
            button_rects = []

            # 显示各关卡选项(两列布局以节省空间)
            for i, level in enumerate(range(1, self.total_levels + 1)):
                cols = 9 + (level - 1) * 4
                if level == 8:
                    cols = 35

                # 两列布局
                col = i % 2
                row = i // 2
                button_width = 280
                button_height = 50
                start_x = self.screen_width // 2 - 300
                button_x = start_x + col * 320
                button_y = y_pos + row * 65

                button_rect = pygame.Rect(button_x, button_y, button_width, button_height)

                # 高亮选中的关卡
                if level == level_var:
                    pygame.draw.rect(self.screen, self.YELLOW, button_rect)
                    pygame.draw.rect(self.screen, self.GRAY, button_rect, 3)
                else:
                    pygame.draw.rect(self.screen, self.LIGHT_GRAY, button_rect)
                    pygame.draw.rect(self.screen, self.GRAY, button_rect, 2)

                text = self.font_normal.render(f"第 {level} 关 ({cols}×{cols})", True, self.BLACK)
                text_rect = text.get_rect(center=button_rect.center)
                self.screen.blit(text, text_rect)

                button_rects.append((button_rect, level))

            # 确认按钮
            confirm_button = pygame.Rect(self.screen_width // 2 - 100, y_pos + 280, 200, 45)
            pygame.draw.rect(self.screen, self.GREEN, confirm_button)
            pygame.draw.rect(self.screen, self.DARK_GREEN, confirm_button, 3)
            confirm_text = self.font_normal.render("确认", True, self.WHITE)
            confirm_text_rect = confirm_text.get_rect(center=confirm_button.center)
            self.screen.blit(confirm_text, confirm_text_rect)

            pygame.display.flip()

            # 事件处理
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    return False
                elif event.type == pygame.MOUSEBUTTONDOWN:
                    for button_rect, level in button_rects:
                        if button_rect.collidepoint(event.pos):
                            level_var = level
                    if confirm_button.collidepoint(event.pos):
                        self.current_level = level_var
                        selecting = False
                        return True
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
                        return False
                    elif event.key == pygame.K_RETURN:
                        self.current_level = level_var
                        selecting = False
                        return True

            self.clock.tick(30)

        return True

    def initialize_level(self):
        """初始化关卡"""
        self.cols, self.rows, self.cell_size = self.get_level_params(self.current_level)

        # 游戏元素
        self.player_x = 1
        self.player_y = 1
        self.end_x = self.cols - 2
        self.end_y = self.rows - 2

        # 重置自动移动
        self.is_auto_moving = False
        self.auto_path = []
        self.auto_step = 0

        # 生成迷宫
        self.generate_maze()

        # 开始计时
        self.start_time = time.time()
        self.game_state = "playing"

    def generate_maze(self):
        """使用递归回溯算法生成迷宫"""
        self.maze = [[1 for _ in range(self.cols)] for _ in range(self.rows)]
        self._recursive_backtrack(1, 1)

        self.maze[self.player_y][self.player_x] = 0
        self.find_farthest_endpoint()
        self.maze[self.end_y][self.end_x] = 0

    def _recursive_backtrack(self, x, y):
        """递归回溯算法"""
        self.maze[y][x] = 0

        directions = [(0, -2), (0, 2), (-2, 0), (2, 0)]
        random.shuffle(directions)

        for dx, dy in directions:
            new_x = x + dx
            new_y = y + dy

            if (0 < new_x < self.cols - 1 and
                    0 < new_y < self.rows - 1 and
                    self.maze[new_y][new_x] == 1):
                wall_x = x + dx // 2
                wall_y = y + dy // 2
                self.maze[wall_y][wall_x] = 0

                self._recursive_backtrack(new_x, new_y)

    def find_farthest_endpoint(self):
        """找到最远端点"""
        queue = deque([(self.player_x, self.player_y, 0)])
        visited = set()
        visited.add((self.player_x, self.player_y))

        farthest_point = (self.player_x, self.player_y)
        max_distance = 0

        directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]

        while queue:
            x, y, distance = queue.popleft()

            if distance > max_distance:
                max_distance = distance
                farthest_point = (x, y)

            for dx, dy in directions:
                new_x = x + dx
                new_y = y + dy

                if (0 <= new_x < self.cols and
                        0 <= new_y < self.rows and
                        (new_x, new_y) not in visited and
                        self.maze[new_y][new_x] == 0):
                    visited.add((new_x, new_y))
                    queue.append((new_x, new_y, distance + 1))

        self.end_x, self.end_y = farthest_point

    def draw_maze(self):
        """绘制迷宫"""
        self.screen.fill(self.WHITE)

        # 计算画布偏移(居中显示)
        maze_width = self.cols * self.cell_size
        maze_height = self.rows * self.cell_size
        self.offset_x = (self.screen_width - maze_width) // 2
        self.offset_y = 100  # 为信息栏预留空间

        # 绘制迷宫格子
        for y in range(self.rows):
            for x in range(self.cols):
                x1 = self.offset_x + x * self.cell_size
                y1 = self.offset_y + y * self.cell_size

                if self.maze[y][x] == 1:
                    pygame.draw.rect(self.screen, self.BLACK, (x1, y1, self.cell_size, self.cell_size))
                else:
                    pygame.draw.rect(self.screen, self.WHITE, (x1, y1, self.cell_size, self.cell_size))
                    # 只在格子较大时绘制边框,避免视觉混乱
                    if self.cell_size > 5:
                        pygame.draw.rect(self.screen, self.LIGHT_GRAY, (x1, y1, self.cell_size, self.cell_size), 1)

        # 绘制终点
        self.draw_endpoint()

        # 绘制玩家
        self.draw_player()

        # 绘制信息栏
        self.draw_info_bar()

    def draw_player(self):
        """绘制玩家"""
        radius = max(3, self.cell_size // 3)
        x = self.offset_x + self.player_x * self.cell_size + self.cell_size // 2
        y = self.offset_y + self.player_y * self.cell_size + self.cell_size // 2
        pygame.draw.circle(self.screen, self.GREEN, (x, y), radius)
        pygame.draw.circle(self.screen, self.DARK_GREEN, (x, y), radius, 2)

    def draw_endpoint(self):
        """绘制终点"""
        radius = max(3, self.cell_size // 3)
        x = self.offset_x + self.end_x * self.cell_size + self.cell_size // 2
        y = self.offset_y + self.end_y * self.cell_size + self.cell_size // 2
        pygame.draw.circle(self.screen, self.RED, (x, y), radius)
        pygame.draw.circle(self.screen, self.DARK_RED, (x, y), radius, 2)

    def draw_info_bar(self):
        """绘制信息栏"""
        # 背景
        pygame.draw.rect(self.screen, self.LIGHT_GRAY, (0, 0, self.screen_width, 90))
        pygame.draw.line(self.screen, self.GRAY, (0, 90), (self.screen_width, 90), 2)

        # 关卡信息
        level_text = self.font_normal.render(
            f"第 {self.current_level} 关 ({self.cols}×{self.rows})",
            True, self.BLACK
        )
        self.screen.blit(level_text, (20, 12))

        # 时间显示
        if self.start_time is not None:
            elapsed_time = int(time.time() - self.start_time)
            self.remaining_time = self.time_limit - elapsed_time
            minutes = max(0, self.remaining_time // 60)
            seconds = max(0, self.remaining_time % 60)

            time_color = self.BLACK if self.remaining_time > 60 else self.RED
            time_text = self.font_normal.render(
                f"剩余时间:{minutes:02d}:{seconds:02d}",
                True, time_color
            )
            self.screen.blit(time_text, (20, 50))

        # 自动寻路状态
        if self.is_auto_moving:
            auto_text = self.font_normal.render("自动寻路中...", True, self.BLUE)
            self.screen.blit(auto_text, (self.screen_width - 200, 12))

        # 控制提示
        tips = "方向键/WASD移动 | 空格/回车自动寻路 | R重新开始"
        tips_text = self.font_small.render(tips, True, self.GRAY)
        self.screen.blit(tips_text, (self.screen_width - 400, 55))

    def toggle_auto_move(self):
        """切换自动移动"""
        if self.game_state != "playing":
            return

        if not self.is_auto_moving:
            # 开始自动移动
            self.find_path_to_end()
            if self.auto_path:
                self.is_auto_moving = True
                self.auto_step = 0
                self.last_auto_time = time.time()
            else:
                print("未找到路径!")  # 调试信息
        else:
            # 停止自动移动
            self.is_auto_moving = False

    def find_path_to_end(self):
        """使用BFS找到到终点的最短路径"""
        queue = deque([(self.player_x, self.player_y, [])])
        visited = set()
        visited.add((self.player_x, self.player_y))

        directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]

        while queue:
            x, y, path = queue.popleft()

            if x == self.end_x and y == self.end_y:
                self.auto_path = path
                return

            for dx, dy in directions:
                new_x = x + dx
                new_y = y + dy

                if (0 <= new_x < self.cols and
                        0 <= new_y < self.rows and
                        (new_x, new_y) not in visited and
                        self.maze[new_y][new_x] == 0):
                    visited.add((new_x, new_y))
                    queue.append((new_x, new_y, path + [(new_x, new_y)]))

        # 如果没找到路径,清空路径列表
        self.auto_path = []

    def move_player(self, direction):
        """手动移动玩家"""
        if self.game_state != "playing" or self.is_auto_moving:
            return

        new_x, new_y = self.player_x, self.player_y

        if direction == 'up':
            new_y -= 1
        elif direction == 'down':
            new_y += 1
        elif direction == 'left':
            new_x -= 1
        elif direction == 'right':
            new_x += 1

        # 检查是否撞墙
        if (0 <= new_x < self.cols and 0 <= new_y < self.rows and
                self.maze[new_y][new_x] == 0):
            self.player_x = new_x
            self.player_y = new_y

            # 检查是否到达终点
            if self.player_x == self.end_x and self.player_y == self.end_y:
                self.win_level()

    def win_level(self):
        """通过关卡"""
        self.is_auto_moving = False

        elapsed_time = int(time.time() - self.start_time)
        minutes = elapsed_time // 60
        seconds = elapsed_time % 60

        # 显示通关信息
        showing_win = True
        while showing_win:
            self.screen.fill(self.WHITE)

            # 绘制迷宫背景
            self.draw_maze()

            # 绘制半透明黑色遮罩
            overlay = pygame.Surface((self.screen_width, self.screen_height))
            overlay.set_alpha(200)
            overlay.fill(self.BLACK)
            self.screen.blit(overlay, (0, 0))

            # 显示通关信息
            win_text = self.font_title.render(f"第 {self.current_level} 关通关!", True, self.YELLOW)
            win_rect = win_text.get_rect(center=(self.screen_width // 2, self.screen_height // 2 - 80))
            self.screen.blit(win_text, win_rect)

            time_text = self.font_large.render(f"用时:{minutes}分{seconds}秒", True, self.WHITE)
            time_rect = time_text.get_rect(center=(self.screen_width // 2, self.screen_height // 2 - 20))
            self.screen.blit(time_text, time_rect)

            if self.current_level < self.total_levels:
                next_button = pygame.Rect(self.screen_width // 2 - 150, self.screen_height // 2 + 40, 300, 50)
                pygame.draw.rect(self.screen, self.GREEN, next_button)
                pygame.draw.rect(self.screen, self.DARK_GREEN, next_button, 3)
                next_text = self.font_normal.render("进入下一关 / 回车键", True, self.WHITE)
                next_rect = next_text.get_rect(center=next_button.center)
                self.screen.blit(next_text, next_rect)

                menu_button = pygame.Rect(self.screen_width // 2 - 150, self.screen_height // 2 + 110, 300, 50)
                pygame.draw.rect(self.screen, self.BLUE, menu_button)
                pygame.draw.rect(self.screen, self.DARK_BLUE, menu_button, 3)
                menu_text = self.font_normal.render("返回菜单 / ESC键", True, self.WHITE)
                menu_rect = menu_text.get_rect(center=menu_button.center)
                self.screen.blit(menu_text, menu_rect)
            else:
                complete_text = self.font_large.render("恭喜完成全部 8 关!", True, self.YELLOW)
                complete_rect = complete_text.get_rect(center=(self.screen_width // 2, self.screen_height // 2 + 40))
                self.screen.blit(complete_text, complete_rect)

                menu_button = pygame.Rect(self.screen_width // 2 - 150, self.screen_height // 2 + 100, 300, 50)
                pygame.draw.rect(self.screen, self.BLUE, menu_button)
                pygame.draw.rect(self.screen, self.DARK_BLUE, menu_button, 3)
                menu_text = self.font_normal.render("返回菜单 / ESC键", True, self.WHITE)
                menu_rect = menu_text.get_rect(center=menu_button.center)
                self.screen.blit(menu_text, menu_rect)

            pygame.display.flip()

            # 事件处理
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    return False
                elif event.type == pygame.MOUSEBUTTONDOWN:
                    if self.current_level < self.total_levels:
                        if next_button.collidepoint(event.pos):
                            self.current_level += 1
                            self.initialize_level()
                            showing_win = False
                        elif menu_button.collidepoint(event.pos):
                            self.game_state = "menu"
                            showing_win = False
                    else:
                        if menu_button.collidepoint(event.pos):
                            self.game_state = "menu"
                            showing_win = False
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
                        self.game_state = "menu"
                        showing_win = False
                    elif event.key == pygame.K_RETURN:
                        if self.current_level < self.total_levels:
                            self.current_level += 1
                            self.initialize_level()
                        else:
                            self.game_state = "menu"
                        showing_win = False

            self.clock.tick(30)

        return True

    def game_over(self):
        """游戏超时"""
        self.is_auto_moving = False

        showing_over = True
        while showing_over:
            self.screen.fill(self.WHITE)

            # 绘制迷宫背景
            self.draw_maze()

            # 绘制半透明黑色遮罩
            overlay = pygame.Surface((self.screen_width, self.screen_height))
            overlay.set_alpha(200)
            overlay.fill(self.BLACK)
            self.screen.blit(overlay, (0, 0))

            # 显示超时信息
            over_text = self.font_title.render(f"第 {self.current_level} 关超时!", True, self.RED)
            over_rect = over_text.get_rect(center=(self.screen_width // 2, self.screen_height // 2 - 60))
            self.screen.blit(over_text, over_rect)

            tip_text = self.font_normal.render("请重新开始", True, self.WHITE)
            tip_rect = tip_text.get_rect(center=(self.screen_width // 2, self.screen_height // 2 - 10))
            self.screen.blit(tip_text, tip_rect)

            restart_button = pygame.Rect(self.screen_width // 2 - 150, self.screen_height // 2 + 40, 300, 50)
            pygame.draw.rect(self.screen, self.GREEN, restart_button)
            pygame.draw.rect(self.screen, self.DARK_GREEN, restart_button, 3)
            restart_text = self.font_normal.render("重新开始 / 回车键", True, self.WHITE)
            restart_rect = restart_text.get_rect(center=restart_button.center)
            self.screen.blit(restart_text, restart_rect)

            menu_button = pygame.Rect(self.screen_width // 2 - 150, self.screen_height // 2 + 110, 300, 50)
            pygame.draw.rect(self.screen, self.BLUE, menu_button)
            pygame.draw.rect(self.screen, self.DARK_BLUE, menu_button, 3)
            menu_text = self.font_normal.render("返回菜单 / ESC键", True, self.WHITE)
            menu_rect = menu_text.get_rect(center=menu_button.center)
            self.screen.blit(menu_text, menu_rect)

            pygame.display.flip()

            # 事件处理
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    return False
                elif event.type == pygame.MOUSEBUTTONDOWN:
                    if restart_button.collidepoint(event.pos):
                        self.initialize_level()
                        showing_over = False
                    elif menu_button.collidepoint(event.pos):
                        self.game_state = "menu"
                        showing_over = False
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
                        self.game_state = "menu"
                        showing_over = False
                    elif event.key == pygame.K_RETURN:
                        self.initialize_level()
                        showing_over = False

            self.clock.tick(30)

        return True

    def run(self):
        """主游戏循环"""
        running = True

        while running:
            if self.game_state == "menu":
                running = self.show_menu()
                if running and self.game_state == "playing":
                    self.initialize_level()

            elif self.game_state == "playing":
                # 检查超时
                if self.start_time is not None:
                    elapsed_time = int(time.time() - self.start_time)
                    self.remaining_time = self.time_limit - elapsed_time

                    if self.remaining_time <= 0:
                        running = self.game_over()
                        continue

                # 自动移动
                if self.is_auto_moving:
                    current_time = time.time()
                    if current_time - self.last_auto_time >= self.auto_speed:
                        if self.auto_step < len(self.auto_path):
                            next_x, next_y = self.auto_path[self.auto_step]
                            self.player_x = next_x
                            self.player_y = next_y
                            self.auto_step += 1
                            self.last_auto_time = current_time

                            # 检查是否到达终点
                            if self.player_x == self.end_x and self.player_y == self.end_y:
                                running = self.win_level()
                                continue
                        else:
                            self.is_auto_moving = False

                # 绘制
                self.draw_maze()
                pygame.display.flip()

                # 事件处理
                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        running = False
                    elif event.type == pygame.KEYDOWN:
                        if event.key == pygame.K_UP:
                            self.move_player('up')
                        elif event.key == pygame.K_DOWN:
                            self.move_player('down')
                        elif event.key == pygame.K_LEFT:
                            self.move_player('left')
                        elif event.key == pygame.K_RIGHT:
                            self.move_player('right')
                        elif event.key == pygame.K_w:
                            self.move_player('up')
                        elif event.key == pygame.K_s:
                            self.move_player('down')
                        elif event.key == pygame.K_a:
                            self.move_player('left')
                        elif event.key == pygame.K_d:
                            self.move_player('right')
                        elif event.key == pygame.K_RETURN or event.key == pygame.K_SPACE:
                            self.toggle_auto_move()
                        elif event.key == pygame.K_r:
                            self.initialize_level()

                self.clock.tick(self.fps)

            else:
                running = False

        pygame.quit()
        sys.exit()


def main():
    game = MazeGame()
    game.run()


if __name__ == "__main__":
    main()
相关推荐
Zzz 小生1 小时前
LangChain Short-term memory:短期记忆使用完全指南
人工智能·python·langchain·github
qq_24218863321 小时前
使用 PyInstaller 打包 Python 脚本为 EXE(教程)
开发语言·python
闪电橘子1 小时前
Pycharm运行程序报错 Process finished with exit code -1066598273 (0xC06D007F)
ide·python·pycharm·cuda
我要七分甜1 小时前
Pycharm中Anaconda的详细配置过程
python·pycharm
Franklin1 小时前
2025-11-28日,天塌了,Pycharm将不开源了!!最后一个开源社区版本2025.2.5
ide·python·pycharm
wx_bishe2881 小时前
python社区流浪动物猫狗救助救援网站_4a4i2--论文_pycharm django vue flask
python·pycharm·django·毕业设计
智慧地球(AI·Earth)1 小时前
在Windows上使用Claude Code并集成到PyCharm IDE的完整指南
ide·人工智能·windows·python·pycharm·claude code
思绪无限1 小时前
使用Conda创建Python环境并在PyCharm中配置运行项目
python·pycharm·conda·安装教程·python环境配置·环境配置教程
前端小旋风1 小时前
pycharm 2025 专业版下载安装教程【附安装包】
python·pycharm