五子棋小游戏Python源码:tkinter实现人机对战与贪心AI详解

发布时间:2026/10/3 15:26:54
五子棋小游戏Python源码:tkinter实现人机对战与贪心AI详解 简介一款基于Python实现的五子棋小游戏源码面向Python初学者与游戏开发爱好者适用于课程设计、课后练手或图形界面编程入门。资源围绕五子棋核心玩法展开涵盖棋盘初始化、棋子绘制、落子判定、胜负逻辑等完整环节代码量精简且注释清晰便于逐行阅读和二次修改。压缩包内共6个文件包含3个Python脚本、2个pyc缓存文件以及1个exe可执行程序整体大小约7.75MBpy文件负责游戏逻辑与交互pyc为Python运行过程生成的字节码缓存exe可直接双击运行适合无Python环境用户快速体验。目前已有375人浏览学习。目录结构简洁以入口模块、棋盘类和主流程文件分层按模块拆解即可掌握五子棋项目的骨架学有余力者可在此基础上扩展人机对战、悔棋、计时或界面美化功能是一个不错的游戏开发起点。1. 五子棋小游戏源码一份能跑通、能改、能搬到网页端的 Python 入门项目很多学 Python 的人卡在一个尴尬位置语法书翻了前三章循环、列表、条件都会写但就是不知道怎么“做出一个东西”。五子棋小游戏 Python 源码恰好是这类困境的解药——它用到的编程基础不超过一维数组、二维列表、循环和函数却能把图形界面、鼠标事件、对弈规则和简单 AI 一次串起来。更反直觉的是五子棋的人机对战根本不需要神经网络或蒙特卡洛搜索一份“四方向评分 攻防权重”的贪心策略就足够让业余玩家认真对待每一手棋。这份源码适合刚学完 Python 基础语法的初学者用来理解 GUI 与算法如何配合也适合想把它改造成五子棋网页版人机的开发者做原型参考。下面按我平时组织这类小游戏源码的完整思路从骨架到避坑一步步拆开讲。2. 源码骨架与最小可运行版本先把窗口、棋盘和落子串起来2.1 源码该拆成几个文件为什么要用面向对象组织网上免费 Python 源码大全里能搜到不少五子棋代码但很多是“一坨脚本”风格几十个全局变量铺在模块顶部函数之间靠全局量互相传递状态。这种写法在最简单的单文件 demo 里能跑一旦你要加悔棋、加 AI、加计时器改动就会一发不可收拾。我组织五子棋源码时一般拆成两个文件核心逻辑全部收进一个类里。文件职责关键内容gobang.py游戏核心类棋盘状态、落子逻辑、胜负判定、AI 选点main.py程序入口创建窗口和画布、绑定鼠标事件、启动消息循环main.py只做界面初始化所有关于“棋盘上现在是什么局面”的信息都存在Gobang类的board二维列表里。0 表示空1 表示黑棋2 表示白棋。界面和状态分离带来的直接好处是后面做 AI 评分时直接读board数组即可完全不用碰 Canvas 绘图逻辑。这种做法也是“源码编程器怎么编出简单游戏”这类问题下最常见的标准答案——先把状态建模做对再谈渲染。2.2 最小可运行代码棋盘渲染与鼠标落子下面这份代码是我会先递给初学者的最小版本它包含窗口、15 路棋盘、鼠标点击落子、黑白交替四个要素没有胜负判定先确保“能跑起来、能看到棋子”。import tkinter as tk SIZE 30 # 每个格子的像素宽度 MARGIN 30 # 棋盘边缘留白 LINES 15 # 15 路棋盘 class Gobang: def __init__(self, root): self.root root self.root.title(五子棋) # 画布尺寸 边缘留白 * 2 格子数 * 格子宽度 self.canvas tk.Canvas( root, widthMARGIN * 2 SIZE * (LINES - 1), heightMARGIN * 2 SIZE * (LINES - 1), bg#E8C89E, ) self.canvas.pack() # board[row][col]0 空 / 1 黑 / 2 白 self.board [[0] * LINES for _ in range(LINES)] self.current_player 1 # 黑棋先行 self.draw_board() self.canvas.bind(Button-1, self.on_click) def draw_board(self): 画网格线注意边界留白 for i in range(LINES): start MARGIN end MARGIN SIZE * (LINES - 1) # 横线 self.canvas.create_line(start, MARGIN i * SIZE, end, MARGIN i * SIZE, fill#000000) # 竖线 self.canvas.create_line(MARGIN i * SIZE, start, MARGIN i * SIZE, end, fill#000000) # 画五个星位天元 四角星 for r, c in [(7, 7), (3, 3), (3, 11), (11, 3), (11, 11)]: self.draw_star(r, c) def draw_star(self, row, col): x MARGIN col * SIZE y MARGIN row * SIZE self.canvas.create_oval(x - 3, y - 3, x 3, y 3, fill#000000) def on_click(self, event): 鼠标落子像素坐标 - 网格坐标 - 画棋子 col round((event.x - MARGIN) / SIZE) row round((event.y - MARGIN) / SIZE) # 点到了棋盘外直接忽略 if row 0 or row LINES or col 0 or col LINES: return if self.board[row][col] ! 0: return self.board[row][col] self.current_player self.draw_piece(row, col, self.current_player) # 切换玩家1 变 22 变 1 self.current_player 3 - self.current_player def draw_piece(self, row, col, player): x MARGIN col * SIZE y MARGIN row * SIZE color #000000 if player 1 else #FFFFFF # 画外圈阴影 内圈主体立体感更好 self.canvas.create_oval(x - 11, y - 11, x 11, y 11, outline#808080, fillcolor) if __name__ __main__: root tk.Tk() Gobang(root) root.mainloop()代码里的几个参数值得说明。SIZE 30决定棋盘整体大小格子像素越大棋子越大但窗口也跟着变大15 路棋盘用 30 到 45 之间视觉最舒服。MARGIN 30是留白它的作用不只是好看——坐标换算round((event.x - MARGIN) / SIZE)必须依赖它才能把鼠标位置对应到正确的格子。这里用round而不是int是刻意的鼠标落到某个格子中心附近时round能把偏移半格的坐标归位到最近的格子上int则永远向下取整点靠右半区时会选错格子。逻辑上最需要注意的部分是self.current_player 3 - self.current_player。玩家 1 和 2 交替3 减任何一方正好得到另一方不需要 if 判断这是小游戏源码里常见且不易出错的写法。board[row][col] ! 0的重复落子拦截也必须放在最前面否则会出现黑棋白棋叠在一个位置上的画面。提示如果你电脑上的 Python 版本低于 3.8建议先做一次 python 安装升级。Windows 安装包默认内置 tkinterLinux 环境则可能要执行sudo apt install python3-tk才会出现tkinter模块。运行这份代码后你会看到一个能点棋子、黑白交替的棋盘但谁赢没人管。下一步就是把胜负判定接进来这是整个源码里最容易翻车的部分。3. 落子与胜负判定从“能下棋”到“判得准”3.1 五连判定的四个方向方向向量与双向扫描五子棋胜负只有一条规则横、竖、两条斜线任一方向先形成连续五个同色棋子即获胜。听起来简单但直接写最容易出两毛病只扫了一个方向导致右侧连五判不出或者数组越界直接抛异常。正确做法是以“刚落下的那颗棋子”为中心沿四个方向各向两边扩散数数。先看四个方向的方向向量。屏幕坐标系里行号向下增大、列号向右增大因此方向行增量 dr列增量 dc对应棋形水平01一行横着连垂直10一列竖着连主对角线11从左上到右下副对角线1-1从右上到左下核心思路是从落子点出发朝一个方向数同色棋子再朝反方向数同色棋子两段加起来加上落子本身那一颗总数为 5 或更多就是胜利。def check_win(self, row, col, player): 以落子点为中心检查四个方向是否形成五连 directions [ (0, 1), # 水平 (1, 0), # 垂直 (1, 1), # 主对角线 (1, -1), # 副对角线 ] for dr, dc in directions: count 1 # 算上刚落下的这颗 # 正方向数过去 r, c row dr, col dc while 0 r LINES and 0 c LINES and self.board[r][c] player: count 1 r dr c dc # 反方向数回来 r, c row - dr, col - dc while 0 r LINES and 0 c LINES and self.board[r][c] player: count 1 r - dr c - dc if count 5: return True return False这个函数的两个细节决定了它是否稳定。第一count初始为 1因为当前落子本身就是一颗第二两次 while 循环都要做边界判断0 r LINES and 0 c LINES一旦越界就停止数数。如果不加边界判断当棋子落在棋盘边上时r dr会一直加到 15 甚至更大随后读取self.board[15][7]必然报 IndexError这也是很多初学者写五分钟棋源码时控制台飘红的最常见原因。3.2 把判定接进落子流程并处理“最后一手不判胜”的细节有了check_win之后把判定挂在on_click里落子之后、切换玩家之前。这里要注意顺序一旦判定当前玩家赢了就不要切换current_player让局面停在胜利的一手棋上方便玩家截图或复盘。from tkinter import messagebox # 在 on_click 里画完棋子后立即判断 if self.check_win(row, col, self.current_player): winner 黑棋 if self.current_player 1 else 白棋 messagebox.showinfo(游戏结束, f{winner}获胜) # 问是否再来一局 if messagebox.askyesno(再来一局, 要重新开始吗): self.reset_board() return # 不再切换玩家 self.current_player 3 - self.current_player重置棋盘函数要同步清空二维数组和画布这是初学者最常漏的环节。清空画布时如果只调用canvas.delete(all)会出现白屏必须紧接着重新执行draw_board()否则网格线全部消失。reset_board的完整实现通常长这样def reset_board(self): self.board [[0] * LINES for _ in range(LINES)] self.canvas.delete(all) self.draw_board() self.current_player 1把reset_board的代码和on_click里的返回逻辑放在一起看会发现“切换玩家”必须放在return之后否则会出现最后一手判胜后颜色仍然变掉的诡异局面。这类顺序问题在 Python 基础语法层面只是 if 与 return 的执行顺序但实际写源码时非常容易忽略。3.3 与后续 AI 对接判定函数演变成“连续数统计”如果你只做双人对弈check_win返回布尔值已经完全够用。但到下一章做人机 AI 时AI 需要知道“某个空位下在这里我能形成几连”而不是简单的“能不能赢”。因此我通常在写check_win的同时再封装一个更底层的工具count_line(row, col, dr, dc, player)返回从某点沿某个方向连续有多少颗同色棋子。def count_line(self, row, col, dr, dc, player): 从 (row, col) 出发沿 (dr, dc) 方向数连续同色棋子数 count 0 r, c row, col while 0 r LINES and 0 c LINES and self.board[r][c] player: count 1 r dr c dc return count注意这个函数不包含反方向扫描它只数单方向。AI 评分时需要分别数四个方向的正反两边然后相加得到“这个空位放在某色的延长线上能形成多长的连子”。这一步铺垫好了后面写 AI 时就不用改任何结构直接拿count_line当搬运工。双人对弈版本到这里已经完整可玩接下来是源码最有吸引力的部分让电脑陪练。4. 人机对战的评分策略让电脑会堵、会攻又不至于无敌4.1 为什么用“贪心评分”而不是搜索树有人一听到人机对战就想到博弈树和 alpha-beta 剪枝但对一个 Python 小游戏源码来说搜索树纯属杀鸡用牛刀——15x15 棋盘每层有几十个落点候选不做复杂度控制AI 每步都要几秒钟娱乐性全无。常见的做法是“贪心评分”遍历每一个空位给这个点分别算一算“我下这里能形成多好的棋形”“对方下这里能形成多危险的棋形”两者按权重相加选分数最高的点落子。五子棋网页版人机里绝大多数默认难度用的就是这套方案。评分函数最核心的一步是把“连续数 两端是否开放”翻译成分数。同样三个连子两端都空活三和只有一端空眠三的威胁完全不同前者下一步就能成四后者可以被堵死。棋形分值说明活四100000两端开放的四连已经不可阻止冲四10000只有一端开放的四连堵住就没事活三5000下一手可发展成活四眠三1000威胁较小但仍需注意活二500可发展为活三的基础棋形眠二100威胁很小4.2 单个空位的评分函数只看四个方向的最大连子数对某个空位评分时要分别站在黑棋和白棋的角度检查如果这里放上黑棋、黑棋能形成的最好棋形值多少分再检查如果这里放上白棋、白棋能形成的棋形值多少分。需要注意这里不能用check_win直接判因为空位还没落子需要先把棋盘对应位置临时改成目标颜色调用四个方向统计后再恢复。def evaluate_point(self, row, col, player): 假设在 (row, col) 下 player 色棋子返回进攻价值 self.board[row][col] player total 0 directions [(0, 1), (1, 0), (1, 1), (1, -1)] max_single 0 for dr, dc in directions: # 正方向 反方向 连子数之和就是这条线上的总连子数 count 1 self.count_line(row dr, col dc, dr, dc, player) \ self.count_line(row - dr, col - dc, -dr, -dc, player) # 检查两端是否开放有多少端是空位 open_ends 0 for sign in (1, -1): nr, nc row sign * dr, col sign * dc if 0 nr LINES and 0 nc LINES and self.board[nr][nc] 0: open_ends 1 # 根据连子数和开放端给分 if count 5: score 100000 elif count 4 and open_ends 2: score 10000 elif count 3 and open_ends 2: score 5000 elif count 3 and open_ends 1: score 1000 elif count 2 and open_ends 2: score 500 else: score 0 total score max_single max(max_single, score) self.board[row][col] 0 # 恢复棋盘 # 返回最大单项分再加上次要分避免多个方向分散的棋形被低估 return total max_single这里count_line(row dr, col dc, dr, dc, player)的正反合并有一个容易搞错的地方四个方向里副对角线(1, -1)的反方向是(-1, 1)所以代码里count_line(row - dr, col - dc, -dr, -dc, player)写成-dr, -dc的形式通用且正确不用单独为副对角线写特例。open_ends统计的是“这条线两端还有多少个空位”两端都空时棋形威胁最大分数自然要高一个量级。4.3 AI 选点与难度控制防守权重大于进攻加随机抖动有了单点评分AI 选点就变成一次全棋盘扫描。遍历所有空位为当前 AI 玩家计算进攻分为对方玩家计算防守分按照“防守权重大于进攻”的原则合并。def ai_move(self): 返回 AI 最佳落点 (row, col) ai_color 2 # 假设 AI 执白 human_color 1 attack_weight 1.0 # 困难模式可调大到 1.3 defend_weight 1.2 # 防守权重高于进攻 best_score -1 best_points [] for r in range(LINES): for c in range(LINES): if self.board[r][c] ! 0: continue attack_score self.evaluate_point(r, c, ai_color) defend_score self.evaluate_point(r, c, human_color) score attack_score * attack_weight defend_score * defend_weight if score best_score: best_score score best_points [(r, c)] elif score best_score: best_points.append((r, c)) # 同分随机选一个避免每次开局走法完全一致 import random return random.choice(best_points)defend_weight默认大于attack_weight是这套策略的灵魂。纯进攻型 AI 的问题在于只顾自己发展对人类的活三视而不见往往在快赢时被人先一步连成五。反之防守权重太高会让 AI 永远被动应对、进攻乏力。我调试这类五子棋 Python 源码时通常把defend_weight放在 1.2 到 1.5 之间这个区间里 AI 既会优先堵对手段又能在局面安全时果断进攻。随机抖动random.choice(best_points)更重要。没有它AI 在多个等分点之间永远选第一个玩几局后你会发现 AI 的开局棋谱一模一样非常容易腻。同分随机不需要额外参数一行代码就能解决。提示评分策略是“贪心”的它只看当前一步的局部得分不会深度推演未来两步以上。所以它打不过专业棋手但作为小游戏陪练足够。想要更强的 AI不是改权重而是引入两步搜索那是另一个层面的话题。5. 五子棋源码的 5 个经典坑坐标、重绘、反向五连与消息循环5.1 坑一棋子画在格子线上永远偏半格现象点击后棋子落在两条线的交叉点旁边歪了大约半格越往棋盘边缘越明显。原因很多人用int((event.x - MARGIN) / SIZE)取整坐标再算出圆心坐标。int是截断取整鼠标点在格子中心偏右一点点时列号被截断到偏小的那格画出的棋子自然落在错误格子上。解决取整改用round((event.x - MARGIN) / SIZE)画棋子时圆心为MARGIN col * SIZE不要加额外偏移。round会把鼠标位置归位到最近的格子中心视觉上落子跟手。col round((event.x - MARGIN) / SIZE) row round((event.y - MARGIN) / SIZE)5.2 坑二横向从左连到右的五连判不出来现象黑棋明明在同一行连了五个从左边第一颗到右边第五颗系统弹不出胜利提示。原因判断方向时只写了(0, 1)一个方向从落子点向右数到第五颗时如果落子在右侧、棋形在左侧count_line根本数不到那些棋子。解决使用正反双向扫描。check_win里沿每个方向先向正方向数、再向反方向数两段相加。只要保证四个方向都做了双向扫描无论这五个棋子以哪一颗作为落子点判定都能正确统计出连续数。5.3 坑三点击棋盘边缘时控制台直接报 IndexError现象鼠标点在边界线附近程序崩溃退出终端显示IndexError: list index out of range。原因round换算出的坐标可能是-1或15甚至点在最角落时算出的行号、列号同时越界访问board[15][7]必然出错。解决落子前先做边界检查越界直接返回不进入后续逻辑。if row 0 or row LINES or col 0 or col LINES: return这一步必须在访问board[row][col]之前写顺序颠倒等于没写。5.4 坑四调用 canvas.delete 后棋盘变白屏现象点击“再来一局”或触发重绘后窗口一片空白网格线全消失只剩下棋子偶尔残留。原因canvas.delete(all)删除了画布上的所有图形包括棋盘线。如果只删不画自然白屏。更隐蔽的情况是重绘时画了棋盘线但画线代码用了局部变量而不是self.canvas导致创建了第二个画布。解决删除后必须立即重新执行draw_board()且保证整个类共用同一个self.canvas对象。不要把Canvas创建成局部变量。def reset_board(self): self.board [[0] * LINES for _ in range(LINES)] self.canvas.delete(all) self.draw_board()5.5 坑五双击文件秒开秒退窗口一闪而过现象在源码编辑器里运行正常直接双击.py文件时命令行窗口一闪就消失什么都看不到。原因脚本末尾漏了root.mainloop()。tkinter 窗口必须在事件循环里才能持续显示没有mainloop程序执行完最后一行就退出。另一个常见原因是 Python 环境路径不对双击默认用了旧版本解释器。解决确保入口文件以mainloop()收尾且不要用if __name__判断外的缩进问题。更推荐的做法是在源码编程器或终端里用python main.py运行而不是直接双击。在终端运行能直接看到完整的报错信息排错效率高得多。这些坑里前三个属于坐标与算法的边界问题后两个属于 tkinter 生命周期问题。把这五条记住基本能避开 90% 以上五子棋小游戏源码的常见翻车点。6. 进阶验证给源码加悔棋、难度挡位并确认判定没翻车6.1 悔棋用栈记录落点一步回退双人对弈拍了拍桌子表示刚才点错了——这个需求几乎每个玩过的人都提过。实现悔棋不需要复杂设计board已经在记录每次落子你把每次落子的位置和颜色压进栈即可。def undo(self): if not self.move_stack: return row, col, player self.move_stack.pop() self.board[row][col] 0 # 用 tag 精准删除那个棋子而不是全画布删除 self.canvas.delete(fpiece_{row}_{col}) self.current_player player落子时每画一颗棋子都给它绑定唯一 tagcanvas.create_oval(..., tagsfpiece_{row}_{col})。这样悔棋时只删除那一颗不用触碰棋盘线。同时记得把self.move_stack.append((row, col, self.current_player))放在on_click里画棋子之后。悔棋的坑在于 last 一步颜色恢复——pop 出来的 player 就是这手棋真正的主人直接赋给current_player即可不需要 3 - player 反转。6.2 对局验证手法不接受“感觉好像赢了”加了功能后最怕的是“感觉赢了但没判定”。我习惯写一个临时验证脚本绕开鼠标事件直接往board里摆棋形。# test_win.py from gobang import Gobang import tkinter as tk root tk.Tk() game Gobang(root) # 手动摆五连黑棋 for col in range(5): game.board[7][col] 1 assert game.check_win(7, 2, 1) is True # 从中间这颗判断 assert game.check_win(7, 0, 1) is True # 从端头这颗判断 print(水平五连判定 OK)再用同样方式验证垂直、主对角线、副对角线以及“四连 空位”不触发胜利。只有这四类棋形都在代码层验证通过才说明判定逻辑真正可靠。AI 部分也可以用类似手段测试固定摆一个活三局面调用ai_move看它是否选择堵住活三的延伸端。如果 AI 总选进攻位而不是防守位把defend_weight往上调如果 AI 每次都堵错位置检查evaluate_point里open_ends是否把两端都正确识别。我早年做这份五子棋源码时犯过最低级的错误坐标换算用int而不是round导致棋子整体右偏偏了半格全盘棋看起来歪歪扭扭。后来我把“像素坐标转网格坐标”和“网格坐标转像素坐标”分别封装成pixel_to_grid和grid_to_pixel两个小函数所有涉及坐标的代码一律走后两者再也没为此翻过车。加悔棋、加 AI 挡位之前先把你自己的坐标换算独立成函数这是这个源码后续扩展的后悔药。希望帮到你。本文还有配套的精品资源点击获取