资讯详情 Python+Pygame五子棋开发(一):从棋盘逻辑到基础AI
📅 2026/10/15 11:59:21
1. 项目玩法拆解与第一版功能界定五子棋大概是所有开发者接触的第一个“真正游戏”项目。规则一句话就能说完黑白双方轮流在棋盘交叉点落子横、竖、斜任意方向率先连成五子的一方获胜。但真要动手写代码你会发现这个项目把你的数据结构、事件处理、坐标换算、算法基本功全串起来了做完之后对整个编程体系的理解都会升一个台阶。“五子棋游戏一”这个标题里的“一”其实很关键。这说明我们不是一上来就要做一个能跟职业选手过招的完美作品而是要先搭一个结实、好扩展的基础版本。我的第一版规划很简单做一个15×15标准棋盘的双人对战程序鼠标点击落子程序自动判断胜负再顺手加一个基础的人机对战模式。把这三个能力跑通后续再往里面加悔棋、禁手、进阶AI、联网对战都是水到渠成的事。先把这个版本的功能边界定死避免写着写着就失控支持标准 15×15 方格棋盘棋子落在交叉点上。双人模式同一台电脑上两个玩家轮流用鼠标落子。人机模式玩家执黑先行程序执白自动回应。落子后立即检测胜负胜利时在界面上弹出提示并支持重新开局。提供简单的菜单或键盘快捷键来切换模式、重置游戏。为什么要先定这个边界因为“五子棋”这三个字能延伸出去的东西太多了禁手规则、三手交换、五手两打、复杂AI、悔棋、计时器……如果不控制范围第一版就会变成一锅粥。我的做法是先把核心闭环跑通——落子、判断、胜负反馈这三个环节是五子棋的灵魂其他东西都是锦上添花。顺便补一个五子棋规则里常被新手忽略的术语后面讲AI逻辑时也会用到。五子棋里有“活四”两端都开放的四连和“冲四”只有一端开放或形成四四点位的四连、“活三”再走一步能变成活四的三连、“眠三”再走一步只能形成冲四的三连这些概念。第一版判胜负只需要判断“是否连五”但到后面写AI、写禁手逻辑这些术语就是讨论的基础。我还想强调这个项目非常适合作为编程学习的中期里程碑。刚学完循环、数组、函数之后很多人会陷入“学了语法但写不出完整程序”的迷茫。五子棋就是一个恰到好处的练习量——不大到让人放弃不小到让人感觉无聊。2. 技术选型与界面设计思路第一版我选的技术方案是 Python 搭配 Pygame 图形库。Pygame 的入门门槛低、跨平台表现稳定、事件模型清晰对于学习图形界面程序和游戏循环这个概念来说非常友好。而且五子棋的逻辑本身不依赖特定图形库就算你用的是 C系语言配图形库或者 JavaScript 配 Canvas核心思路完全能迁移。拿到这个项目先别急着写代码花十分钟想清楚“棋盘怎么画、棋子落在哪”这两个问题。棋盘是一个15×15的网格每个交叉点就是一个可落子位置。窗口尺寸我定为640×720左边60像素留作边距下面60像素留作状态栏显示当前回合提示。格子边长设为40像素这样棋盘最大可用范围就是15×40等于600像素。坐标换算是这个项目第一个容易出错的点。像素坐标和棋盘逻辑坐标之间的映射关系是这样逻辑坐标row, col对应像素坐标 (col * 40 60, row * 40 60)。反过来鼠标点击位置加上偏移量之后除以40取整就能得到对应的row和col。例如鼠标点在了像素点80, 300上先减去边距得到20, 240再做整除运算得到的逻辑坐标就是6, 0。注意这里容易出现整除边界问题鼠标点在线段边缘时可能误判到相邻格子所以一般还要做一次距离判断只有点击位置距离交叉点的距离小于某个阈值比如格子边长的四成才允许落子。界面方面我习惯把状态信息放在窗口底部的一个固定区域。顶部区域显示当前轮到谁走右侧留一小块区域做模式切换按钮。这种布局可以让玩的人一眼就看清楚当前游戏状态不用靠猜。棋盘背景色我选了偏暖的米黄色黑棋用黑色圆点白棋用白色圆点加一圈深色描边——这个描边不是可有可无纯白棋子在浅色背景上很容易看不清轮廓加一个两像素的深色描边观感立刻上一个档次。还有一个细节是棋子的半径。格子边长40像素的情况下棋子半径设成16比较合适视觉上大概占格子宽度的八成稀疏得当。画棋子时要注意圆点不要超出格子边界否则两个相邻棋子会叠在一起显得拥挤。底部除了状态栏我还会显示“按R重新开始、按Esc退出”这类快捷键提示。这个界面的设计思路很简单别整花活清楚、稳定、反馈即时比什么都重要。很多初学者容易在这上面花太多时间调UI美化结果核心逻辑一直没写这就本末倒置了。3. 核心逻辑实现棋盘存储、落子与胜负判定界面是皮逻辑才是骨。这一步我按三层来做棋盘状态的存储、落子的交互处理、胜负判定算法。每一层都很独立也方便后面测试和扩展。棋盘存储我用的是一个二维数组15行15列初始值全是0。0表示空位1表示黑棋2表示白棋。很多教程喜欢用字符串或者字典来存储棋盘但我建议用二维数组因为后面无论是判断胜负、扫描空位、还是做AI评估二维数组都是最直观、性能也够用的结构。board [[0 for _ in range(15)] for _ in range(15)] current_player 1 # 1黑棋, 2白棋落子的交互处理逻辑放在主循环里。Pygame 的主循环每一帧都在跑我们需要监听鼠标事件一旦发生点击就做三步把像素坐标换算成棋盘坐标、检查该位置是否为空、把棋子写入棋盘并刷新显示。for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.MOUSEBUTTONDOWN: x, y event.pos col (x - 60) // 40 row (y - 60) // 40 if 0 row 15 and 0 col 15 and board[row][col] 0: board[row][col] current_player draw_piece(row, col, current_player) # 网格越界检查和空位检查都放这里这里有两个很常见的坑。第一是坐标转换时没做边界检查。如果点在棋盘区域外比如点到了右侧状态栏减去60再整除40就会得到一个超出0到14范围的行列号直接访问数组会报错。第二是没做格子归属容差导致点两个格子交界处时落到相邻格子。我的处理方式是拿到行列号后先检查有效性再算该交叉点的精确像素坐标计算点击点和交叉点的距离大于某个阈值就忽略这次点击。胜负判定是整个项目的核心也是我这次想重点讲透的部分。判断五子棋胜负的方法有很多落子后全盘扫描、对每个棋子朝四个方向统计连子数等等。我采用的做法是“以最后一次落子为中心检测四个方向”。具体思路是这样刚下的这颗棋子是当前回合的唯一变量如果棋盘上形成了连五这五颗子必然包含刚下的这颗。所以我只需要从这颗棋子出发沿着四条线——水平方向、垂直方向、主对角线方向、副对角线方向——分别向两边数连续同色棋子的数量。每个方向两边的数量加起来再减去当前棋子本身被重复计算一次如果大于等于5说明已经赢了。def count_direction(board, row, col, dx, dy, player): count 1 # 当前棋子自身 for step in range(1, 5): nr, nc row step * dx, col step * dy if 0 nr 15 and 0 nc 15 and board[nr][nc] player: count 1 else: break for step in range(1, 5): nr, nc row - step * dx, col - step * dy if 0 nr 15 and 0 nc 15 and board[nr][nc] player: count 1 else: break return count def check_win(row, col, player): directions [(1, 0), (0, 1), (1, 1), (1, -1)] for dx, dy in directions: if count_direction(board, row, col, dx, dy, player) 5: return True return False有的读者会问为什么向两个方向数的时候每次循环只走4步因为我只需要判断是否连成五子。连五不可能是从当前棋子出发数完了单边五颗还没有到达边界所以单边最多数四步就够了。当然你也可以写成数到棋盘边界为止我没那么做是为了让“差一步形成禁手的判断”在后续版本里更容易扩展。这个胜负判定方案的优点在哪里第一时间复杂度低。最坏情况下只需检查四条线、每条线最多八步远快于全盘扫描方案。第二边界条件好控制。越界检查写在内部循环里不会数组越界。第三它天然支持后面要增加的禁手判断——只需把“数连续同色棋”的逻辑稍微改动就能统计出活三、冲四这些棋形的数量。我特别想提醒一个初学者容易踩的坑很多人统计连续同色棋子时会用一个全局变量max_count然后在循环里不断更新它结果在双方向累加时把当前棋子重复计了两次。我在代码里使用局部变量count先初始化为1再分别向两边累加有效避开了这个问题。写这类逻辑的时候脑子里要有一幅图你是站在当前棋子上向左右各探出头去而不是每走一步就从头数一遍。4. 基础人机AI逐点评分法的实现很多人做完双人对战模式后最大的成就感来自“让我和电脑下一盘棋”。第一版的人机AI我用了逐点评分法这是五子棋AI里最容易理解、也最能打出效果的算法之一。它不涉及搜索树不涉及剪枝但足以打败没怎么研究过五子棋的普通玩家。原理非常直白电脑拿到棋盘后遍历每一个空位对每个空位分别打分——假设黑棋落在这里有多大的威胁值假设白棋也就是电脑自己落在这里有多大价值。然后把这两个维度合并成一个总分。最后电脑选择总分最高的那个空位落子。听起来像笨办法但实际效果相当不错因为五子棋的棋形评估天然适合这种“局部打分”策略。打分规则我按棋形的重要程度来定参考五子棋里棋形的基本价值排序棋形进攻分说明连五100000直接获胜最高优先级活四10000两端都开放对方无法阻挡冲四1000差一步成五对手必须堵活三1000可发展成活四威胁很大眠三100只能形成冲四威胁一般活二100有发展潜力但需要时间眠二10基本属于试探性棋形这个是经典的“评分表”做法实际分值可以微调但相对次序别打乱。我在代码里用了一个函数给定一个位置模拟把这个棋子放下去然后在四个方向统计连子数和开放程度根据这两个指标去查表得到分值。注意这个过程中要区分“空位是正常空位”和“空位是边界外”开放端的定义是“紧挨着棋形的那一格是空位”如果那格是对方棋子或棋盘边界就属于封闭端。AI的最终打分公式是这样的def evaluate_point(board, row, col, ai_player): human_player 3 - ai_player score 0 # 进攻分模拟AI自己落子 board[row][col] ai_player score evaluate_shape(board, row, col, ai_player) # 防守分模拟对手落子 board[row][col] human_player score evaluate_shape(board, row, col, human_player) * 0.9 board[row][col] 0 # 恢复空位 return score防守分乘以0.9是一个很实用的经验值。为什么不乘1或更高因为你是在“替对方想一步”不等于对方真的会下这里如果防守权重过高电脑会变得畏首畏尾全程堵你但不进攻棋局很容易下成僵局。乘以0.9会让电脑在威胁足够大时果断防守同时在机会来临时敢于进攻。然后AI遍历整个棋盘把所有空位都评一遍分找到最高分的那个位置落子best_score -1 best_move None for row in range(15): for col in range(15): if board[row][col] 0: s evaluate_point(board, row, col, ai_player) if s best_score: best_score s best_move (row, col)这个算法最妙的地方在于“它完全知道应该堵谁的棋”。当你在棋盘上摆了个活三电脑扫描空位时会发现如果它不堵你在下一步就能形成活四这个位置对应极高的防守分所以它会毫不犹豫地堵在关键位置。当它自己有机会形成连五时进攻分更是直接爆表这时它会果断下出制胜一手。第一版AI我故意没做成更高级的Alpha-Beta剪枝搜索。原因有三第一逐点评分法已经满足第一版游戏的需求棋力足够应付日常娱乐第二评分法好调试出问题一眼能看出来是哪个方向漏算了而搜索树一旦写错问题很难定位第三后续文章里我会在上面扩展搜索深度先把评分体系打扎实后面的代码就能直接复用。还有一个细节要处理AI如果在开局阶段就按照评分表逐点扫描会在最中央几个点随机选一个。这个行为虽然正确但略显呆板我在实现时给AI加了一个小逻辑——开局前三步如果棋盘上没有子那就先下到中心点附近稍微靠边一点的位置。这会让开局变化多一点不容易一开局就走入重复定式。5. 常见问题与调试技巧实录我一开始写这个项目时踩了不少坑也帮一些朋友看过他们写的版本总结出了几个高频问题。如果你实际运行游戏时碰到了意外可以参考下面的排查思路。问题一Pygame窗口启动后立刻闪退。这个问题的原因几乎都是主循环没有正确运行。很多人写完界面初始化代码就直接开始画棋子忘记写while running:这个主循环或者循环条件一开始就是 False窗口画完一帧就退出了。解决办法是确认代码结构大致是这样初始化、进入主循环、事件处理、更新逻辑、重绘图像、刷新屏幕。一个简单的自查方法是在主循环里加一句打印如果控制台没有周期性输出就说明主循环根本没跑起来。问题二落子的棋子和网格线对不齐。这种情况几乎都是坐标换算公式写错了。你可以把公式单独拆出来测试给定几个逻辑坐标手动算出期望的像素坐标然后和执行结果对比。我强烈建议在开发阶段给每个交互函数写一个简单的测试函数输入固定坐标输出固定结果出问题一眼就能看出来。坐标换算这类纯函数级的逻辑既不容易上下文耦合也适合单元测试。问题三胜负判定在四条线上“数多了”。一个常见的错误是在某个方向上数完连续同色棋子之后又跑到另一个方向把所有同色棋子重新数一遍最终数量被重复计算。比如你在水平方向上数到五颗子已经赢了但代码没返回继续去数垂直方向把重叠的棋子又加了一次。解决办法是在check_win里一旦检测到某个方向返回的连续数大于等于5立即返回True不做多余的统计。我在调试胜负判定时用过一个非常笨但非常有效的方法强行在一个远离其他棋子的位置摆一个五连场景然后让程序打印每一步检测的三个方向——左边数量、右边数量、总数量。如果某个数字明显不对基本就能锁定是哪一段逻辑出错了。这种“人肉打印调试法”看起来很土但在纯逻辑型代码里比什么抽象工具都好使。问题四AI全程只防守不进攻棋局特别被动。这多半是防守权重设置过高造成的。你可以把防守分系数从0.9往0.8调再测试几局。如果调到0.6以下AI又变得太激进、容易漏掉明显的威胁那就在0.8到0.9区间内调整。这类参数没有标准答案和你的棋形评分表数值搭配有关调参时每次只改一个变量改完马上对局验证。问题五白棋在浅色棋盘上看不清楚。解决办法前面提到过给白棋加一圈深色描边。Pygame里画圆时可以先用深灰色画一个半径稍大的圆再叠加一个白色小圆视觉上就形成了一圈描边效果。这个优化对局体验提升非常明显成本却几乎为零。还有一条重要的工程建议把棋盘状态实时打印出来。print函数在图形编程里的价值被严重低估。你可以在每次落子后把整个15×15的二维数组打印到控制台1代表黑棋2代表白棋0代表空位。这样哪怕图形界面渲染出了bug控制台里棋盘状态仍然一目了然。我第一版做完之后后面调AI参数的效率一大部分就靠这个打印功能撑着。在调试过程中另一个让我印象深刻的坑是画棋子时没有调用pygame.display.flip()导致棋子画上了屏幕却不显示看起来就像落子失败。图形程序里“绘制”和“呈现”是两回事绘制是把指令发给后备缓冲区呈现才是把缓冲区内容真正显示到屏幕上。每次用pygame.draw画完东西记得刷一次屏。这种低级问题排查起来特别费时间最好的规避方式就是在开发初期写一个固定模板把绘制、更新、呈现的顺序固化下来别乱改。6. 从第一版到后面的扩展我的一些实操心得做完这个“五子棋游戏一”的基础版本整体的成就感其实比想象中更大。因为从零把一个能正常对战的游戏写出来需要的不只是代码能力还有拆解问题、控制范围、调试排错的综合功夫。下面分享几个我总结的个人心得这些在教程里很难找到。第一点是“先让版本能跑再让版本好玩”。我在第一版里没有执意追求花哨的AI效果和复杂的界面动画而是把双人对战、判胜、基本人机这三个功能做扎实。中途有几次我想给棋盘加背景音乐、加落子动画考虑到那会让代码复杂不少还是忍住了。事实告诉我第一版越简洁后续第二版、第三版加功能时就越有信心因为你已经搭好了一个不会到处连环崩溃的地基。第二点是“控制台是裸眼3D调试的利器”。这个项目图形界面和逻辑混合在一起如果不把逻辑层数据可视化出了问题很难定位。我养成了一个习惯棋盘状态每次变化后都打印一遍虽然打印输出看起来很原始但它让我在五分钟内就能判断问题是出在“逻辑没写入数组”还是“数组有值但界面没画出来”。实操时你还会发现一个好处这种打印内容可以直接复制下来作为和同行讨论问题时的依据比截图方便十倍。第三点是评分法AI虽然不算高级但可玩性比你想象得高。我测试时发现只要防守和进攻权重设置得当这个AI足以在低端对局中保持不败。它的极限在于没有前瞻性看不清两三步之后的杀棋机会但这恰恰是后续优化的切入点。后面我计划在评分法的基础上加一层深度搜索让AI在选出得分最高的前几个位置后再往前搜索后续两步看哪条路线对自己更有利。到那时评分表本身不用重写只需要加一个搜索壳而已。这就是逻辑分层的价值——核心评估函数稳定你就能在其上不断叠加新能力而不动根基。如果你也正在写自己的五子棋版本我的建议是先完整跑通双人对战模式再动AI。很多人一上来就写AI结果棋盘逻辑本身还有各种隐藏bug后期排查时根本分不清是AI的问题还是基础逻辑的问题会非常痛苦。把基础逻辑验证扎实AI只是在这个可靠基础上做一次选择你调试起来会从容很多。这个系列后续可以做的事还有很多增加悔棋和重新开局按钮、实现不同难度级别的AI、加入落子音效和动画、把棋盘做成可缩放的窗口、甚至加入简单的网络对战功能。每一步都建立在这个第一版的基础之上。如果你对这个项目有任何想法或者卡住了非常欢迎留言一起讨论。