ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python五子棋游戏源码:tkinter界面、事件绑定与AI算法全解析

Python五子棋游戏源码:tkinter界面、事件绑定与AI算法全解析 简介基于Python实现的五子棋小游戏完整工程包非常适合Python入门学习者、游戏开发爱好者以及需要课程设计或期末项目参考的同学。项目采用清晰的客户端/服务端架构包含server.py、client目录及lib、data等模块既支持本地双人对弈也可扩展为联网对战练习。压缩包共19个文件其中9个py脚本为主要逻辑代码pyw入口便于直接运行png与ttf提供界面图标和字体资源ogg与mp3用于游戏音效附带README.md说明文档整体压缩体积仅5.4MB结构精简适合快速上手。目前已有1051人浏览学习说明该资源具备一定关注度。通过这份工程读者可以学到棋盘绘制、落子判定、胜负逻辑以及基础网络通信的Python实现思路还能直接运行体验游戏并在此基础上修改界面或增加AI对手是动手实践与二次开发的良好起点。1. python小游戏之五子棋一套代码吃透界面、事件与规则先说结论五子棋几乎是“第一个完整小游戏”的最佳选择。棋盘逻辑简单到不用查数据结构但把 tkinter 界面绘制、鼠标事件绑定、规则判定、简单 AI 四块全占了。网上搜 python 五子棋源码能找到的开头几乎都是import tkinter as tk from tkinter import messagebox但很多只画了个棋盘就没了判定和 AI 丢给你自己去补。这份资源不一样的地方在于它是能直接从第一颗子下到分出胜负的完整工程带电脑对手输了会弹窗告诉你结果。适合刚学完 python 基础语法、想从“敲例子”跨到“写程序”的人也适合拿它当 tkinter 授课素材的老师或家长。不需要你有 pygame 基础不需要装额外依赖python 环境里自带 tkinter。2. 棋盘绘制与事件绑定先解决“棋子怎么画、点哪里落子”五子棋程序的第一步不是逻辑是界面。你必须有地方让玩家看到棋盘、看到棋子、知道自己点中了哪个位置。这部分选型很关键为什么不选 pygamepygame 做像素级控制更强但需要单独安装而且事件循环是它自己的和 tkinter 的思路不同。tkinter 是 python 自带的标准库不需要pip install双击就能跑对五子棋这种低帧率、单击落子的场景完全够用。python 环境本身装没装好不在本文范围装好后命令行跑一句python --version验证即可。2.1 棋盘尺寸与坐标换算棋盘我用标准的 15×15 路也就是交叉点 15 行 15 列。这里有个关键设计画布上每个交叉点的间距必须固定否则后面坐标换算全是坑。我一般这样定义基础参数import tkinter as tk from tkinter import messagebox SIZE 15 # 棋盘路数15 路是标准五子棋 CELL 30 # 相邻交叉点间距单位像素 MARGIN 30 # 棋盘到画布边缘的距离 RADIUS 12 # 棋子半径 CANVAS_SIZE MARGIN * 2 (SIZE - 1) * CELL # 480逻辑说明SIZE 决定棋盘是 15 路还是 19 路改成 19 就是标准大棋盘练习小棋局用 9 也行。CELL 影响棋子的稀疏程度。屏幕不够大可以把 CELL 调到 24但 RADIUS 也要跟着缩。CANVAS_SIZE 的计算方式是边距 ×2 加上 (SIZE-1) 个间距因为 15 个交叉点只有 14 段间距。所有变量用大写命名是告诉读者这是全局配置落子逻辑和绘制逻辑都统一引用不会出现“这里硬编码了 480那边又写 460”的问题。2.2 从像素坐标到行列索引鼠标点击拿到的是画布上的像素坐标比如x135, y75。你要把它换算成第几行第几列这一步是五子棋最容易翻车的地方。公式是col round((x - MARGIN) / CELL)row round((y - MARGIN) / CELL)。用 round 而不是 int因为点击落在两个交叉点之间时要就近吸附到最近的那个交叉点上。def pixel_to_index(self, x, y): col round((x - MARGIN) / CELL) row round((y - MARGIN) / CELL) if row 0 or row SIZE or col 0 or col SIZE: return None, None return row, col逻辑说明如果不用 round 而用 int点击交叉点右下方半格会被强制吸附到上一格产生肉眼可见的错位。换算之后必须做越界检查。棋盘边缘有半个格子宽的空白点击那里得到的 row 或 col 可能超出 0~14直接返回 None 让上层忽略。这里返回的是row, col注意行列顺序。后面所有棋盘数组都按board[row][col]访问如果你在初始化数组时反着写二维数组会直接越界报错。2.3 棋盘与棋子绘制绘制本身分成两层棋盘底图和棋子。棋盘底图用 Canvas 的create_line画 15 条横线、15 条竖线再在正中心、四个角和四条边中点画 5 个小圆点当星位。棋子用create_oval填黑或白两色黑棋加一层灰色描边视觉上更立体。self.cv tk.Canvas(self.root, widthCANVAS_SIZE, heightCANVAS_SIZE, bg#d9a76a) self.cv.bind(Button-1, self.click_handler) self.cv.pack() # 画线 for i in range(SIZE): start, end MARGIN i * CELL, MARGIN (SIZE - 1) * CELL self.cv.create_line(MARGIN, start, end, start) # 横线 self.cv.create_line(start, MARGIN, start, end) # 竖线逻辑说明Canvas 的bg参数用了类似原木色#d9a76a比纯白好看而且不影响落子判断因为判断只依赖board数组不依赖图像像素。Button-1是鼠标左键事件绑定到click_handler上。右键不绑留作扩展比如右键悔棋。画线时注意参数顺序create_line(x1, y1, x2, y2)两条线分别控制横线和竖线循环写在一起坐标计算要用 MARGIN i * CELL 对齐。落子绘制和状态更新是两件事先检查该位置是否为空为空才把棋盘数组置为当前棋子然后画圆。def click_handler(self, event): if self.game_over: return row, col self.pixel_to_index(event.x, event.y) if row is None or col is None: return if self.board[row][col] ! 0: return self.board[row][col] self.current_player self.draw_piece(row, col) if self.check_win(row, col): messagebox.showinfo(游戏结束, f{黑棋 if self.current_player 1 else 白棋}获胜) self.game_over True else: self.current_player 3 - self.current_player def draw_piece(self, row, col): px MARGIN col * CELL py MARGIN row * CELL color black if self.board[row][col] 1 else white self.cv.create_oval(px - RADIUS, py - RADIUS, px RADIUS, py RADIUS, fillcolor, outlinegray)逻辑说明self.current_player用 1 表示黑棋、2 表示白棋切换玩家只需3 - current_player不用写 if/else。点击事件三步走坐标换算 → 检查占用 → 落子绘制。任何一步不满足都直接return不污染棋盘数组。这个顺序不要颠倒。game_over标志加在事件函数第一行是防止游戏已经结束时还能继续落子。draw_piece 本身不用知道当前是谁它只读board[row][col]的值决定颜色状态只从数组读这是后面所有逻辑能跑对的前提。棋盘数组初始化为二维列表全 0self.board [[0 for _ in range(SIZE)] for _ in range(SIZE)]注意这行不能用[[0] * SIZE] * SIZE因为后者生成的每一行都是同一个列表对象的引用你改board[0][1]会连带改掉board[1][1]。这条坑我踩过后面避坑章节专门再讲。3. 胜负判定四方向连珠检测的关键写法界面能落子了但程序根本不知道谁赢了。五子棋判定在棋类程序里属于“写起来很短、写错却很隐蔽”的逻辑。核心思路很简单每次落子后从落子点出发沿横、竖、左斜、右斜四个方向分别检查连续同色棋子的数量任意方向连续数 ≥5 就判胜。3.1 方向向量与连续计数四个方向用方向向量表示(dr, dc)表示每次移动的行列增量。四方向就是DIRECTIONS [(1, 0), (0, 1), (1, 1), (1, -1)]这四个向量分别对应向下、向右、右下斜、左下斜。为什么是这四组而不是八组因为每个方向都是“双向”的比如水平方向从落子点左端数到右端一个方向就能覆盖整条水平线。如果你把上下、左右拆成 8 个方向单独数代码会重复一半还容易漏方向。3.2 落子点判定函数def check_win(self, row, col): color self.board[row][col] for dr, dc in DIRECTIONS: count 1 for sign in (1, -1): r, c row sign * dr, col sign * dc while 0 r SIZE and 0 c SIZE and self.board[r][c] color: count 1 r sign * dr c sign * dc if count 5: return True return False逻辑说明sign 取 1 和 -1 分别沿当前方向的正反两端延伸。起点本身算 1 个子所以 count 初始为 1。while 循环的终止条件是出界或者遇到不是当前颜色的棋子。关键点是一旦遇到空位或对方棋子立刻停不能跳着数。五子棋的“连珠”必须是连续的中间断一个位置就不算。每个方向数完count 拿来和 5 比较。注意判断是 5而不是 5因为六个连续也是赢写成 5会漏判。走出棋盘边界时while 条件里的0 r SIZE会先判断不会越界访问数组。3.3 长连与禁手的取舍标准五子棋有禁手规则黑棋三三、四四、长连判负。但对这个练手小游戏我建议直接忽略禁手判胜条件就是“连到五个或更多”。原因有两点禁手判定要额外区分“真活三”“假活三”复杂度会上升一个档次新手容易写崩你和 AI 对战时 AI 的评分逻辑很难严格遵守禁手不做禁手双方规则反而对称。以后要写比赛级引擎再补禁手检测不迟。check_win 之后还要处理一个隐藏分支棋盘下满但没人赢判和棋。def is_full(self): for r in range(SIZE): for c in range(SIZE): if self.board[r][c] 0: return False return True你也可以在每次落子后维护一个move_count计数器步数到 225 就判和棋。这样是 O(1) 判断不用每次遍历全棋盘。3.4 判定函数放在哪里执行判定不需要在每次鼠标移动时执行只在该步落子完成后执行。放在 click_handler 里、draw_piece之后即可。另外有个性能细节判断赢了之后不要立刻弹窗。弹窗是模态的会阻塞事件循环但弹窗前要把game_over True设好避免弹窗期间用户疯狂点击把棋盘打乱。顺序是先标记结束再弹窗而且必须保证只执行一次。提示如果你发现弹窗点掉之后棋盘还能落子一定是game_over标志没设置或没判断。这是五子棋最常见的问题。4. 电脑对手评分函数让 AI 会进攻也会防守纯双人对战的五子棋写完判定就算完成。但很多人是冲着“人机对战”来的所以我补上了 AI。AI 方案没有用复杂的搜索树——蒙特卡洛树搜索和 alpha-beta 剪枝对这个体量的项目是杀鸡用牛刀而且新手难以理解。我用的是一套“评分函数 贪心选点”核心一句话每个空位打一个分分数由进攻价值和防守价值加权求和取最高的落子。4.1 为什么不用搜索树五子棋搜索树的问题在于分支因子太大15×15 棋盘第一步 225 个选择第二步 224 个两层的节点数就有五万个四层就是几亿。要剪枝到可用级别代码复杂度已经不是一份“新手能看懂”的源码了。评分贪心虽然不会算到未来三五步但配合好的评分表实战水平足够跟普通玩家打得有来有回。要更强的 AI后续可以把这里的评分函数换成神经网络评估器接口不用动。4.2 单点评分函数评分的思想是假设我在这落一颗子沿四个方向看每个方向能构成几连连数越多、开口越多分越高。开口指连子两端是否为空位两端都空叫“活”一端为空另一端被堵或出界叫“眠”。def evaluate_point(self, r, c, color): score 0 for dr, dc in DIRECTIONS: count 1 open_ends 0 r1, c1 r dr, c dc while 0 r1 SIZE and 0 c1 SIZE and self.board[r1][c1] color: count 1 r1 dr; c1 dc if 0 r1 SIZE and 0 c1 SIZE and self.board[r1][c1] 0: open_ends 1 r2, c2 r - dr, c - dc while 0 r2 SIZE and 0 c2 SIZE and self.board[r2][c2] color: count 1 r2 - dr; c2 - dc if 0 r2 SIZE and 0 c2 SIZE and self.board[r2][c2] 0: open_ends 1 score self.shape_score(count, open_ends) return score逻辑说明每个方向先数正向、再数反向和 check_win 的遍历方式一致只是这里不判断是否赢而是把 count 和 open_ends 送进评分表换分数。open_ends最多是 2表示两端都空越界的那一端不叫“开口”。分数累加不取最大值。如果一个位置在横竖两个方向都有活三分数会叠加AI 会优先选择这种“交叉威胁”点。4.3 连子形状评分表shape_score 返回单个方向构成的威胁等级。权重是经验值你可以调AI 强不强全在这张表上countopen_ends2活open_ends1眠110050260020038000100045000010000≥5100000100000def shape_score(self, count, open_ends): if count 5: return 100000 if open_ends 2: return {1: 100, 2: 600, 3: 8000, 4: 50000}.get(count, 0) if open_ends 1: return {1: 50, 2: 200, 3: 1000, 4: 10000}.get(count, 0) return 0参数说明五连给 100000 的绝对高分只要出现必然优先。活四给 50000因为活四两端都空下一步对方无论如何挡不住必死局。冲四四连但一端被堵给 10000只能挡一端威胁很大。活三给 8000三连两端都空下一步能变成活四。眠三给 1000威胁较小活二 600用于开局布局。4.4 AI 选点进攻与防守加权AI 落子时对每个空位分别算黑子和白子落在那里的得分黑子得分代表进攻威胁白子得分代表如果对方下在这里对 AI 的威胁。最终得分是两者加权def ai_move(self): best_score -1 best_pos (SIZE // 2, SIZE // 2) for r in range(SIZE): for c in range(SIZE): if self.board[r][c] ! 0: continue if not self.near_existing(r, c): continue attack self.evaluate_point(r, c, 2) defend self.evaluate_point(r, c, 1) score attack * 1.1 defend if score best_score: best_score score best_pos (r, c) return best_pos逻辑说明attack * 1.1 defend里的 1.1 是进攻偏好系数表示比防守略微激进但参数接近不会出现对面活三了 AI 还在自己发展的情况。系数改成 0.8AI 就偏保守防守改成 2AI 会特别激进。near_existing是一个剪枝函数只考虑已有棋子周围两格以内的空位避免全盘 225 个点全部评分。第一手棋盘全空时返回 False直接用棋盘中心。这个优化让 AI 计算量从 225 次评分降到几十次。near_existing 的实现很简单检查以(r, c)为中心半径 2 的范围里是否有非零棋子def near_existing(self, r, c): for dr in range(-2, 3): for dc in range(-2, 3): rr, cc r dr, c dc if 0 rr SIZE and 0 cc SIZE and self.board[rr][cc] ! 0: return True return False4.5 如何把 AI 接到主流程里人机对战时玩家执黑先手点击后切换到 AI 执白。AI 落子要稍微延迟一下否则棋盘上瞬间多一颗子体验很怪。常见做法是加一个定时器比如 400ms 后再让 AI 落子if self.current_player 2 and not self.game_over: self.root.after(400, self.ai_turn) def ai_turn(self): if self.game_over: return row, col self.ai_move() self.board[row][col] 2 self.draw_piece(row, col) if self.check_win(row, col): messagebox.showinfo(游戏结束, AI 获胜) self.game_over True else: self.current_player 1逻辑说明root.after(400, ...)是 tkinter 的定时器单位毫秒表示 400ms 后调用 ai_turn。这里不能用time.sleep()sleep 会阻塞整个事件循环界面会卡死。ai_turn 里必须先调 ai_move 再绘制因为 AI 读的是棋盘当前状态必须在玩家落子并绘制完成后才调用。root.after只是排了个期如果玩家在 400ms 内又点了棋盘click_handler 会以game_over和current_player的状态拦截掉多余点击。5. 五子棋避坑指南坐标、数组引用与事件状态的五处翻车点写五子棋源码踩过的坑比写逻辑本身多。下面这几条是我从调试里一条条抠出来的任何一条都可能导致最终程序看起来“能用但不对”。按现象排查的顺序我把它分成视觉层、规则层、策略层三层来讲每一层都对应实际的翻车记录。5.1 视觉层坐标错位与数组引用别名现象点击交叉点偏右下方一点棋子落在更远的位置另一种情况是下一颗黑棋整行或整列都变成黑棋棋盘瞬间满了。原因坐标错位是int((x - MARGIN) / CELL)造成的。int 是向下取整点击点落在两个交叉点之间时会被强制吸附到左上方那一格而不是最近的交叉点。数组整行变色是初始化方式的问题[[0] * SIZE] * SIZE的乘号复制的是列表引用15 个“行”其实是同一个列表的 15 个别名改一行等于改全行。解决坐标换算用 round并且加一个距离校验点击点与交叉点中心的距离超过半格就判定无效col round((x - MARGIN) / CELL) row round((y - MARGIN) / CELL) if abs(x - (MARGIN col * CELL)) CELL / 2: return None, None数组初始化改用列表推导式self.board [[0 for _ in range(SIZE)] for _ in range(SIZE)]逻辑说明半格判断的代码执行顺序在坐标换算之后、访问self.board之前。这个校验的作用是防止玩家点在两个交叉点中间的空隙时被强行吸附处理 19 路棋盘时尤其重要因为格子更密误触概率更高。至于数组推导式它是每行新建独立列表互不干扰。5.2 规则层连珠漏判误判与弹窗后还能落子现象横向明明连了五子程序不弹胜利弹窗或者中间隔着一个空位却判赢了又或者 messagebox 点掉之后棋盘还能继续落子。原因漏判是count 5写成了 5六连不判误判是 while 循环判定了self.board[r][c] ! 0而不是 color把对方棋子也算进连珠。弹窗问题则是game_over标志没有在事件函数开头做守卫只在弹窗分支里赋值。解决判定循环严格写成“等于当前颜色才继续数”一旦遇到空位或对方棋子立即停下while 0 r SIZE and 0 c SIZE and self.board[r][c] color: count 1 r sign * dr c sign * dc事件函数首行加守卫def click_handler(self, event): if self.game_over: return ...逻辑说明 color和! 0只差一个字符但语义完全不同。前者只统计当前落子的颜色遇到对方棋子会立刻中止这正是连珠的定义后者会把对方棋子也纳入计数出现“隔山打牛”式的误判。弹窗守卫要放在事件函数第一行game_over在弹窗前赋值。我踩过一次把game_over赋值放在弹窗之后结果弹窗期间鼠标还能点弹窗关闭后棋盘已经被改了两三次。5.3 策略层AI 首手异常与空棋盘评分现象AI 先手时第一步下到 (0, 0) 或棋盘边缘后面一直被压制或者 AI 明明可以五连获胜却跑去堵对方。原因空棋盘时evaluate_point返回 0所有空位分数相同best_score初始为 -1 时任意空位都成立最后遍历到的往往在右下角。第二个现象是进攻权重配比不对defend 分在加权里压过了 attackAI 变成了“只守不攻”。解决首手直接返回棋盘中心后续再走评分逻辑权重系数调成attack * 1.2 defend并确保 evaluate_point 里把五连分设成 100000保证有必杀机会时 AI 不会跑偏def ai_move(self): if not self.existing_pieces(): return SIZE // 2, SIZE // 2 ...逻辑说明首手特判不需要太复杂用中心点即可。真正需要调的是权重系数玩三局感受一下如果 AI 老是在快赢的时候堵对方说明 attack 权重太低如果 AI 对对方的活三视而不见说明 defend 权重太低。评分表里的 100000 是“绝对优先级”只要存在五连机会它的分数必然大于其他所有活三活四的组合这个值不要改小。5.4 出问题先查这一层一条实测排查顺序接触过不下二十份五子棋源码问题最后基本都落在三层界面层查坐标换算、状态层查 game_over 时序、规则层查 color 判断。我给自己定了一个排查顺序先用最快的方式定位问题场景现象优先检查关键变量棋子落错位置pixel_to_index 的取值函数round(int)、CELL棋盘变花board 初始化方式列表推导式判定不弹窗check_win 的 count 条件 5判定乱弹while 循环的颜色判断 color弹窗后还能点click_handler 守卫game_overAI 第一步乱跑ai_move 首手特判SIZE // 2注意调试 AI 时先把 near_existing 的半径从 2 改成 3问题更容易暴露但计算量会翻倍。跑通后再改回 2 提升速度。6. 进阶收尾悔棋、打包与参数微调三件套到这里你的五子棋已经能完成“人机对战→判定→弹窗”的完整循环。想让这份源码真正达到“拿得出手”的状态我建议再做三件小事。第一件事是悔棋。加一个 move_stack 列表每次落子把(row, col, player)压栈。悔棋时弹出最近两步AI 一步和玩家一步把对应棋盘数组归零重绘整个棋盘。重绘就是清空 Canvas 全部元素然后调用一个 draw_board 方法重画线和所有残留棋子——draw_board 就是把第 2 章那段画线循环和星位抽出来的方法。清空全部再重画最多几十毫秒比逐颗擦除简单得多。def undo_move(self): if not self.move_stack: return for _ in range(2): if self.move_stack: row, col, _ self.move_stack.pop() self.board[row][col] 0 self.cv.delete(all) self.draw_board() self.current_player 1 self.game_over False第二件事是打包成 exe这是很多人问“能不能发给朋友玩”的刚需。安装 pyinstaller 后执行pyinstaller -F -w wuziqi.py-F 表示打包成单个文件-w 表示不弹控制台窗口。打包出来的 exe 在 dist 目录下目标机器不需要装 python。最近很多人问 python 生成 exe 可执行文件怎么弄其实就是这一条命令的事。第三件事是参数微调。游戏的核心手感全在配置文件里想让 AI 更强把attack * 1.1改成attack * 1.5并调大活三权重想让节奏更快把 after 的 400 改成 150想做成 19 路SIZE 改 19Canvas 尺寸会自动跟着变。所有这些改动都不需要碰逻辑函数。我自己的习惯是每次改完参数后强制把三个场景各玩一遍玩家斜线赢、AI 冲四反杀、平局收场。之前有一次只测了横线赢就跑过来了结果对 AI 代码做递归改动时把对角线方向写坏发出去给别人玩才发现。从那以后我每次写小游戏测完正常流程还要把“弹出结果后继续点击”的边界点一遍确认事件真的被拦住了。希望这份五子棋源码的思路和坑能帮你也少走这几趟弯路。带注释的完整源码在下载包里跑一遍十分钟比对着教程从零敲要省事得多。本文还有配套的精品资源点击获取
返回列表