新闻详情

新闻详情

首页 / 资讯中心 / 详情

用Pygame从零实现扫雷游戏:核心逻辑与界面绘制全解析

发布时间:2026/9/29 9:04:09来源:尧图网络
用Pygame从零实现扫雷游戏:核心逻辑与界面绘制全解析
写这个扫雷项目其实是我带几个新手朋友学Python时的产物。当时他们刚啃完基础语法正处在“能看懂、不会写”的尴尬期需要一个既有完整逻辑闭环、又能立刻看到直观效果的项目来练手。选来选去扫雷是最合适的规则全世界都懂数据模型不复杂但真要把它写得逻辑严密、玩起来顺手里面涉及的边界处理、状态管理、事件分发这些知识点足够一个初学者消化好一阵子。这篇文章就把我从零搭这个项目的完整过程记录下来包括每一步为什么这么设计、代码怎么写、以及实测中踩过的坑希望能给想用Pygame做小游戏练手的朋友一点参考。1. 整体设计与思路拆解动手写代码之前先把扫雷这个游戏在脑子里抽象成几个核心问题数据怎么存、规则怎么判、界面怎么画、鼠标怎么响应。把这四件事想清楚了写代码就是填空。1.1 为什么用Pygame而不是其他方案Python做图形界面可选方案其实不少。Tkinter是标准库自带的胜在不用装依赖PyQt功能强大但学习曲线偏陡Pygame则是游戏开发里最常用的一套它的定位很明确——就是帮你处理窗口创建、帧率控制、图片绘制和事件捕捉这些底层琐事把主要精力留给游戏本身的逻辑。我最终选Pygame主要看重三点。第一它的逻辑是“死循环驱动事件”的模式和写网页、写脚本的思路完全不同能帮新手建立游戏开发特有的编程思维。第二它跨平台Windows、macOS、Linux都能跑代码完全一致读者在自己电脑上复现的时候不会因为系统差异卡住。第三Pygame对素材的要求极低扫雷这种方块游戏连图片都不需要直接用矩形填充加文字就能把界面做得像模像样省去了很多资源准备的工作量。安装也是一个pip指令的事pip install pygame装完在Python交互环境里跑一句pygame.ver能输出版本号就说明环境没问题了。1.2 扫雷游戏的核心规则抽象扫雷的规则可以简化为三个部分。棋盘是一个二维网格格子分两类有雷的和没雷的。每个没雷的格子旁边也就是它周围8个格子的雷总数就是它翻开时要显示的数字。玩家的操作就两种左键翻开一个格子右键标记一个疑似有雷的格子。如果翻开的格子是雷游戏直接结束如果翻开的格子周围没有雷就自动扩散把相连的空白区域全部翻开如果所有的非雷格子都被翻开了游戏胜利。这个规则看起来简单但真要代码实现有两个点是新手最容易绕晕的。第一个是“自动扩散”的逻辑——你点一个空格子它要一波接一波往外翻这个动作在编程里叫“洪泛填充”需要用递归或队列来实现。第二个是“输赢判断”——到底是数翻开的格子数还是判断未翻开的非雷格子数用错了条件会导致游戏在有雷的地方卡死。1.3 数据模型设计如何用一个二维数组撑起整个游戏我设计数据模型的原则是游戏的逻辑计算和界面的绘制完全分离。也就是说逻辑层只管数据不关心某个格子画在屏幕哪个像素位置界面层只负责把数据画出来不关心数据是怎么算出来的。基于这个原则我用三个二维数组管理棋盘状态全部用列表推导式初始化# 每个元素是0-8的数字代表周围雷的数量用-1表示该位置是雷 mine_map [[0 for _ in range(cols)] for _ in range(rows)] # 每个元素记录格子的显示状态 # 1表示已翻开0表示未翻开2表示被玩家标记了红旗 revealed [[0 for _ in range(cols)] for _ in range(rows)] # 标注雷的位置布雷阶段用计算完就没用了 mines [[False for _ in range(cols)] for _ in range(rows)]这是我踩了几个版本迭代后觉得最清晰的结构。每个数组的职责单一谁都不会越界管别人的事。mine_map负责告诉你一个格子翻开后该显示什么数字revealed负责告诉你哪些格子已经被翻开、哪些还没动过、哪些被标记了mines只在布雷阶段使用布完雷生成mine_map后它的使命就完成了。这里也正好给新手提个醒能用列表推导式初始化二维数组千万不要用[[0] * cols] * rows这种方式。后者创建出来的每一行其实是同一个列表对象的引用你改一个格子的值整列都会跟着变这个坑我当年就踩过排查了半天才反应过来。2. Pygame核心概念与工程搭建正式写扫雷逻辑之前得先弄明白Pygame的底层工作方式。很多新手一开始就把代码堆在一起结果窗口白屏、程序卡死根本原因是对Pygame的运行机制没有概念。2.1 Pygame窗口与坐标系工作原理Pygame的窗口本质上是一个坐标系。左上角是原点(0, 0)x轴向右增加y轴向下增加单位是像素。这一点和数学课上的坐标系不太一样所以新手经常出现画图位置偏了的情况——你以为是向上移动结果元素跑到了屏幕下方。扫雷棋盘在这种坐标系里处理起来很直观假设每个格子边长是cell_size像素那第row行、第col列的格子的左上角坐标就是(col * cell_size, row * cell_size)。因为整个棋盘是规则排列的不需要任何复杂的坐标计算直接乘就行了。窗口的创建也是固定套路三步走pygame.init() # 初始化所有Pygame模块 screen pygame.display.set_mode((width, height)) # 创建指定尺寸的窗口 pygame.display.set_caption(扫雷) # 设置窗口标题第一句pygame.init()很多人会忘。它负责初始化Pygame内部的所有模块包括显示、声音、字体等不调用它就直接创建窗口程序直接报错。这个设计其实是Pygame为了保证各模块的加载顺序而设的统一入口写任何Pygame程序都应该第一步调用它。2.2 事件循环机制游戏活起来的秘密Pygame游戏的核心是一个死循环叫“主循环”。它做的事就三件处理事件、更新游戏状态、绘制画面。循环一遍叫一帧帧率越高动画就越流畅。扫雷虽然是回合制游戏不需要高帧率但主循环的结构不能省。clock pygame.time.Clock() # 创建一个时钟对象 running True while running: for event in pygame.event.get(): # 获取本帧所有事件 if event.type pygame.QUIT: # 点了窗口的关闭按钮 running False elif event.type pygame.MOUSEBUTTONDOWN: # 鼠标按下 handle_mouse_click(event) # 处理游戏逻辑 draw_board() # 根据最新状态绘制画面 pygame.display.flip() # 把绘制的画面显示到屏幕上 clock.tick(60) # 控制帧率为60FPS我不建议新手一上来就优化事件处理的结构。先把pygame.event.get()这个函数理解透就够了它会把从上一次调用开始积累的所有事件一次性取出来然后你逐个遍历判断类型做出响应。这叫“事件驱动”和写Web后端一条路径跑到头完全不同。很多新手写主循环的时候容易漏掉pygame.display.flip()。Pygame的设计是先在后台的缓冲面上绘制绘制完成后调用flip()一次性把缓冲内容显示到屏幕上。如果忘了这一步窗口会一直停留在最初创建时的黑色画面你绘制的所有内容都看不到。2.3 工程文件结构单文件起步的合理边界对于这个项目的规模我觉得单文件就够了。把数据初始化、逻辑计算、界面绘制、事件处理这几个模块用函数区分开一个main.py装下所有代码总共才两百多行阅读和调试都很方便。什么时候该拆成多文件当你发现某个函数超过50行、或者一个文件超过300行的时候再考虑按功能拆分。比如拆成game.py核心逻辑、board.py界面绘制、main.py入口和事件循环。过早地拆分文件只会让新手把精力浪费在研究 import 路径上对理解核心逻辑没有帮助。开发过程中还会用到几个辅助工具。比如调试时想打印棋盘数据我会临时写一个函数把mine_map和revealed一起打印出来def debug_print(): for row in range(rows): line for col in range(cols): if revealed[row][col]: line str(mine_map[row][col]) \t elif mines[row][col]: line M\t else: line ?\t print(line) print(---)这个工具在整个开发过程中帮我省了大量时间很多看似“灵异”的bug把棋盘状态一打印立刻就现出原形了。3. 扫雷核心逻辑实现数据模型有了工程框架搭起来了接下来就是重头戏怎么把扫雷的规则变成一个一个可执行的函数。3.1 布雷算法随机撒雷的正确姿势布雷的逻辑很直接从所有格子中随机挑出mine_count个位置把mines数组对应位置设为True。看起来简单但实现方式有好几种效果差异很大。最直观的写法是“双重循环随机碰运气”import random placed 0 while placed mine_count: row random.randint(0, rows - 1) col random.randint(0, cols - 1) if not mines[row][col]: mines[row][col] True placed 1这个写法的优点是代码简单缺点也明显如果棋盘很大而雷很少while循环碰运气的效率还不错但如果棋盘接近饱和比如5x5的棋盘布20个雷越到最后越难找到空位循环次数会急剧增加。更严谨的做法是把所有坐标放进一个列表用random.sample一次性抽取出不重复的坐标all_positions [(r, c) for r in range(rows) for c in range(cols)] mine_positions random.sample(all_positions, mine_count) for r, c in mine_positions: mines[r][c] True第二种写法的性能是稳定的不管雷多雷少都是一次抽取完事。而且random.sample保证了抽取结果不重复不需要手动去重。这也是我个人更推荐的方案。布完雷之后就该计算每个格子周围的雷数了。这一步的代码是初学者第一个容易写晕的地方因为要处理“边界”问题——棋盘边缘的格子没有8个邻居。def calculate_numbers(): # 八个方向的偏移量 directions [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)] for row in range(rows): for col in range(cols): if mines[row][col]: mine_map[row][col] -1 continue count 0 for dr, dc in directions: nr row dr nc col dc if 0 nr rows and 0 nc cols and mines[nr][nc]: count 1 mine_map[row][col] count这里有个小技巧值得说用“方向偏移量”列表来遍历邻居比手写8个if判断要干净得多而且不容易漏。以后你写别的游戏只要涉及到九宫格范围的计算都可以用这个模式。3.2 翻开格子的递归扩散扫雷最核心的算法当你点到一块空白区域的时候游戏会自动把相连的空白格子全部翻开这就是扫雷最爽快的机制也是代码难度最大的地方。我用递归来实现这个洪泛填充逻辑。递归的思路是我翻一个格子如果这个格子周围没有雷那么我周围的8个邻居也都要翻开对每个邻居重复这个过程。如果某个邻居已经被翻开过那就跳过防止死循环。def reveal_cell(row, col): # 超出边界直接返回 if not (0 row rows and 0 col cols): return # 已经翻开或标记了就不管 if revealed[row][col] 1 or revealed[row][col] 2: return # 翻到雷了游戏结束 if mine_map[row][col] -1: game_over() return # 翻开当前格子 revealed[row][col] 1 # 如果周围没有雷递归翻开所有邻居 if mine_map[row][col] 0: for dr in (-1, 0, 1): for dc in (-1, 0, 1): if dr 0 and dc 0: continue reveal_cell(row dr, col dc)说到递归新手最大的恐惧就是“会不会栈溢出”。扫雷棋盘一般最大是30x30900个格子递归的深度最多也就900层Python的默认递归限制是1000层理论上卡着边。但实际运行中因为边界条件限制很少会真正达到这个深度。如果你把棋盘设置成99x99这种超级大棋盘再用递归确实可能爆栈。到那时候就需要改成用“队列循环”的迭代写法但那是进阶优化的话题了正常玩扫雷用递归完全够。3.3 输赢判定与游戏状态管理输赢判定做得不好游戏会出现“明明把雷标完了却不结束”“明明所有格子都翻开了还在继续玩”之类的诡异情况。我的判定条件是这样的失败翻开的格子是雷这个在reveal_cell里已经处理了。胜利所有非雷格子全部被翻开也就是翻开的格子数量等于总格子数减去雷的数量。def check_win(): revealed_count 0 for row in range(rows): for col in range(cols): if revealed[row][col] 1: revealed_count 1 return revealed_count rows * cols - mine_count注意这里的细节是数“已翻开的格子数”而不是数“剩余未翻开的格子数”。两者差别在于被标记为红旗的格子既不是翻开状态、也不是什么特殊状态如果你数“未翻开的格子”那被标了红旗的格子永远不会计入就会导致永远赢不了。这是我当时踩的坑现在写出来提醒各位。游戏状态我用一个简单的整数来管理# 0进行中1胜利2失败 game_state 0状态变量要放在主循环外面定义让它在整个游戏生命周期内都有效。每次玩家点击后先判断game_state是否为0不是0就忽略所有点击操作防止游戏结束后还能继续翻格子。3.4 右键标记与安全区保护右键标记功能说难不难说简单也有细节。我设计了三种状态循环切换未翻开 → 红旗 → 问号 → 未翻开。这个循环给玩家提供了“不确定但先标记”的中间状态很实用。def toggle_flag(row, col): if revealed[row][col] 0: revealed[row][col] 2 # 标记为红旗 elif revealed[row][col] 2: revealed[row][col] 3 # 标记为问号 elif revealed[row][col] 3: revealed[row][col] 0 # 取消标记这里有个很容易出现的bug如果右键点击了已翻开的格子应该什么都不做。上面的代码因为条件里限定了revealed[row][col] 0才处理所以天然避开了这个问题。但我见过不少初版代码没做这个限制结果玩家在已翻开的数字上点右键数字莫名其妙变成了问号体验很差。另外空格子周围的雷数计算里有一个“坎”第一次点击就踩雷怎么办传统扫雷的做法是“第一次点击永远安全”我的实现是如果第一次点击就踩雷就把雷转移到别的位置。具体做法是在布雷时留一个安全区或者点击后再调节雷的位置。考虑到文章篇幅我用的方案是等玩家第一次点击后再真正布雷布雷时避开第一次点击的坐标及其周围8个格子。这个处理会稍微增加代码复杂度但游戏体验好太多建议有条件一定要做。4. 界面绘制与交互细节逻辑层写完游戏已经“能玩”了但你看不到它在干什么——因为还缺界面层。这一部分要把二维数组的数据翻译成屏幕上的画面。4.1 绘制棋盘数格子到像素点的映射绘制棋盘的核心就是遍历revealed数组根据格子的状态决定怎么画。我在实现时给不同状态分配了不同的颜色未翻开的格子用灰色、已翻开的用白色、旗子和问号在上面叠加标记、数字则用不同颜色区分大小。def draw_board(): screen.fill(BG_COLOR) for row in range(rows): for col in range(cols): x col * cell_size y row * cell_size # 未翻开 if revealed[row][col] 0 or revealed[row][col] 2 or revealed[row][col] 3: pygame.draw.rect(screen, UNREVEALED_COLOR, (x, y, cell_size, cell_size)) pygame.draw.rect(screen, GRID_COLOR, (x, y, cell_size, cell_size), 1) # 边框 if revealed[row][col] 2: draw_flag(x, y) # 画红旗 elif revealed[row][col] 3: draw_question_mark(x, y) # 画问号 else: # 已翻开 pygame.draw.rect(screen, REVEALED_COLOR, (x, y, cell_size, cell_size)) pygame.draw.rect(screen, GRID_COLOR, (x, y, cell_size, cell_size), 1) # 如果是雷画雷的图案否则画数字 if mine_map[row][col] -1: draw_mine(x, y) elif mine_map[row][col] 0: draw_number(x, y, mine_map[row][col])有个细节想提醒给每个格子画边框第四个参数传入线宽时线宽为1会导致两个相邻格子之间画两条线视觉上会有重叠变粗的效果。解决办法有两个一是边框只画右边和下边二是让格子的绘制区域略小于cell_size。我实际测试下来第二种效果更好看代码也简单pygame.draw.rect(screen, GRID_COLOR, (x 1, y 1, cell_size - 2, cell_size - 2), 1)数字的颜色我用了一套经典的扫雷配色方案1是蓝色、2是绿色、3是红色、4是深蓝色、5是棕色、6是青色、7是黑色、8是灰色。这个颜色分配是Windows扫雷流传下来的玩家看到颜色就知道数字大小算是默认的行业标准了。4.2 文字绘制Pygame字体模块的正确用法Pygame绘制文字用到pygame.font模块。一个很常见的错误是直接在游戏主循环里反复调用pygame.font.SysFont()创建字体对象这样做非常消耗性能。正确的做法是在初始化阶段创建一次字体对象之后反复使用。font_big pygame.font.SysFont(None, 36) # 大号字体用于游戏结束提示 font_num pygame.font.SysFont(None, 28) # 数字字体 font_flag pygame.font.SysFont(None, 24) # 标记字体如果指定的字体系统里没有SysFont会回退到默认字体所以字体名称可以传None让Pygame自动选择一个合适的中文字体。在开发中我用英文数字和符号进行绘制配合None字体在中英文系统上都能稳定显示。绘制文字到屏幕上需要两步先用font.render()把文字渲染成图片Surface再用blit把图片贴到指定位置。def draw_number(x, y, num): text font_num.render(str(num), True, NUMBER_COLORS[num]) # 居中显示 text_rect text.get_rect(center(x cell_size // 2, y cell_size // 2)) screen.blit(text, text_rect)blit是Pygame里最常用的绘图操作它的作用简单说就是“把一张图贴到另一张图上”。记住get_rect(center...)这个用法可以避免手工计算文字的左上角坐标Pygame会自动帮你居中。4.3 鼠标点击的坐标换算从像素到格子鼠标事件里给的是像素坐标我们要把它换算成“第几行第几列”然后才能通过数组索引找到对应格子。这个换算其实就是一次整数除法def handle_mouse_click(event): mouse_x, mouse_y event.pos col mouse_x // cell_size row mouse_y // cell_size # 检查是否点到了棋盘范围内 if not (0 row rows and 0 col cols): return if event.button 1: # 左键 reveal_cell(row, col) elif event.button 3: # 右键 toggle_flag(row, col)注意event.pos返回的是包含两个元素的元组(x, y)对应窗口坐标。如果是用的缩放窗口坐标可能和棋盘的实际像素位置有偏差——这个问题我在开发中没遇到因为我的窗口大小和棋盘尺寸是固定的。但如果有人做了窗口缩放功能就需要额外做坐标变换。还有一点容易疏忽左键点击已经翻开的格子应该判断它的周围是否满足快速翻开条件。传统扫雷有个“双击数字”的功能——如果数字周围已经标记了相同数量的旗子双击数字会直接翻开周围的未标记格子。这个功能能极大提升游戏速度但实现起来要额外判断很多边界考虑到文章定位是“从0到1实现完整逻辑”我没有把它放进主流程留给读者作为进阶扩展。4.4 状态栏与辅助信息展示除了棋盘本身我还给游戏增加了一个状态栏放在窗口顶部宽和高按比例分配。状态栏显示三样东西剩余雷数、游戏状态文字、重新开始按钮。剩余雷数的计算方式是mine_count减去当前标记的红旗数。注意这里不需要实时统计只用维护一个计数器每次点击左键/右键时更新即可。这个数值可能为负——玩家标记了比实际雷数更多的地方——但要允许这种情况出现不能强制限制在0以上因为玩家标记多了也是一种合法操作程序应当允许但不推荐。重新开始按钮我直接给它画成一个固定的矩形区域并监听鼠标点击是否落在区域内。这个按钮的实现和普通游戏按钮的逻辑一样对初学者是一个很好的微小练习。# 判断是否点击了重新开始按钮 if BUTTON_RECT.collidepoint(event.pos): reset_game()collidepoint是Pygame矩形对象提供的方法直接判断一个点是否在矩形范围内比我手动比较x和y坐标要简洁得多。5. 完整流程串联与问题排查实录代码模块都写完接下来就是把它们串联起来跑起来。这个阶段一定是问题高发期——至少我是这么过来的前前后后改了将近十轮才得到一个稳定运行的版本。5.1 从初始化到主循环代码的完整运行流程我的main.py运行流程如下分三段。初始化阶段创建窗口、设置标题、初始化字体、设置棋盘参数、调用reset_game()生成初始棋盘。这里的reset_game()是一个综合函数它把mine_map、revealed、mines全部重置并调用布雷和数字计算的函数。设计成“一个函数完成重置”的好处是以后做“重新开始”按钮时直接复用同一个函数就行不用再写一遍初始化逻辑。主循环阶段遍历事件、调用对应处理函数。事件处理我专门拆成handle_mouse_click这个函数逻辑层和界面层的耦合就靠这个函数解耦。主循环本身保持极简只有事件处理、画面绘制、帧率控制三行核心代码。收尾阶段退出主循环后调用pygame.quit()释放Pygame占用的资源。这个动作平时没感觉但在某些系统上如果不调用程序结束后会出现后台残留进程侵占了端口或资源所以规范上一律要写。def main(): pygame.init() screen pygame.display.set_mode((WIN_WIDTH, WIN_HEIGHT)) pygame.display.set_caption(扫雷) clock pygame.time.Clock() font_big pygame.font.SysFont(None, 48) global game_state reset_game() running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.MOUSEBUTTONDOWN: handle_mouse_click(event) draw_board() draw_status_bar(font_big) pygame.display.flip() clock.tick(60) pygame.quit() if __name__ __main__: main()if __name__ __main__:这行代码是Python的一个惯例它的意思是“只有当你直接运行这个文件时才执行下面的函数”。如果你在其他文件里import这个文件main()不会被自动调用这能避免很多意外的重复执行。5.2 新手最容易翻车的4个运行问题写这个项目带人过程中我被问过最多的问题集中在几个固定的点上我列成表格直接给出症状和解决方案。问题现象可能原因解决方案窗口闪一下就消失主循环写成了单次执行没有while循环确保主循环是while running:无限循环窗口黑屏看不到棋盘缺少pygame.display.flip()在绘制完成后调用flip()刷新屏幕点击没反应事件处理里用了event.type pygame.MOUSEBUTTONDOWN但没在循环内遍历确认pygame.event.get()的遍历循环没有缩进错误棋盘数据错乱多个格子显示同一数字初始化二维数组时用了[[0] * cols] * rows改用列表推导式[[0 for _ in range(cols)] for _ in range(rows)]这些问题的共同点是报错信息往往不明显程序能跑但行为不对。所以我强烈建议开发阶段配合debug_print函数使用每完成一个功能模块就打印一次棋盘数据确认数据正确后再画界面这样排查问题的范围会小很多。5.3 代码优化小技巧让游戏运行更流畅Pygame默认启用双缓冲但如果你显示的帧率过高比如clock.tick(60)之后还叠加了其他循环可能会导致画面闪烁。解决方式很简单在初始化后调用screen.set_alpha(None)或者在绘制时统一使用pygame.display.flip()而不是update()。另外如果棋盘尺寸很大比如30x30每次重绘所有格子其实是不小的开销。一个简单的优化是只在发生变化的格子区域调用pygame.draw.rect重新绘制其他区域不动。但对于扫雷这种点击频率很低的游戏逐帧全量重绘完全够用不需要过度优化。真正影响性能的反而是文字渲染——如果你在draw_number里频繁创建字体对象和渲染文字帧率会明显下降。我的经验是字体对象一律在初始化时创建好渲染操作保持在最小范围。5.4 复盘项目完成后我给自己列的几个扩展方向基础版完成后我列了几个可以继续加功能的扩展方向给读者做进阶练习参考。这些方向都不改变原来的核心架构只是在已有逻辑上添加模块。第一个是计时器功能。在状态栏显示从第一次点击开始消耗的时间超过一定时间游戏失败或玩家主动挑战更快通关时间。实现方案是在主循环里累计帧数简单可靠。第二个是难度选择菜单。初级9x9、10雷中级16x16、40雷高级30x16、99雷。思路是点击菜单按钮后重新设置棋盘参数并调用reset_game()局部的函数复用能省不少事。第三个是自定义皮肤。用图片替换现在的色块让每个格子有更精致的视觉效果。实现方案是把draw_board里的填充色和文字绘制替换成blit图片这一改动只影响界面层逻辑层完全不需要动。第四个是排行榜功能。把胜利时间和雷数记录到文件里下次打开游戏能加载显示。这涉及文件读写对初学者来说也是一个新的练习模块。结尾主体代码写到这里扫雷的核心功能都已经跑通了。回顾整个开发过程我最大的体会有两点。第一个体会是做小游戏项目一定不要一上来就写代码。哪怕是最简单的扫雷也值得花十分钟在纸上把数据结构和状态转换画清楚。我在第一个版本里没有经过这个步骤直接边想边写结果逻辑改了三遍、界面重构了两次浪费的时间远比画图多。后来养成“先画图再写码”的习惯很多坑在设计阶段就避免了。第二个体会是调试能力真的是写出来的。二十分钟写完一个模块可能得花两小时找里面的小bug。但只要耐心地打印数据、逐步跟进这些bug反而会变成最有价值的学习素材。我对边界条件的敏感度就是在扫雷这个项目里练出来的。最后分享一个小技巧如果你在调试递归扩散的时候出现“点一个格子整个棋盘全翻开”的情况多半是你在某个方向的偏移量里写错了数字把(-1, 0)写成了(-1, 1)导致你的相邻格子判定范围比想象的大了一圈。这种问题用 debug 打印配合肉眼比对很快就能发现。希望这个项目能帮你迈出用 Python 做游戏的第一步玩得开心更写得开心。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

分布式电源并网对配电网电流保护的影响与整定实战指南 2026/9/29 9:04:07

分布式电源并网对配电网电流保护的影响与整定实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
张高兴的大模型开发实战:(六)在 LangGraph 中使用 MCP 协议配 TaoToken 2026/9/29 9:04:00

张高兴的大模型开发实战:(六)在 LangGraph 中使用 MCP 协议配 TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Windows下C++单线程多端口select模型实战与避坑指南 2026/9/29 9:04:00

Windows下C++单线程多端口select模型实战与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
基于人工神经网络的配电网小电流接地选线方法研究 2026/9/29 9:04:00

基于人工神经网络的配电网小电流接地选线方法研究

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
华为手机连电脑没反应?数据线到驱动的全链路排查指南 2026/9/29 9:04:00

华为手机连电脑没反应?数据线到驱动的全链路排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
目标检测从入门到实战:算法流派、经典论文与选型指南 2026/9/29 9:04:00

目标检测从入门到实战:算法流派、经典论文与选型指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉