ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python+Tkinter五子棋项目实战:GUI设计、坐标吸附与胜负判定详解

2026/9/17 9:44:07 拓冰建站 浏览量
Python+Tkinter五子棋项目实战:GUI设计、坐标吸附与胜负判定详解 简介面向计算机专业学生的Python毕业设计参考以五子棋游戏为选题完整覆盖需求分析、可行性研究、系统设计、Tkinter界面实现、胜负判定、系统测试与问题解决等环节适合作为课程设计或本科论文写作模板。压缩包共1个doc文档大小约253KB文档内包含论文正文及基于Tkinter库的源码片段、界面展示和测试记录无需额外安装环境即可对照阅读。已有629人浏览学习资源原创性声明明确代码逻辑围绕可视化模块、玩家操作模块、胜负判定模块展开并讨论了保存进度、悔棋等扩展需求及性能优化思路。读者可借此快速把握五子棋游戏从需求到落地的完整开发流程同时获得毕业设计写作结构、算法实现和排错经验等多重参考。1. 为什么选 PythonTkinter 做五子棋毕业设计一套 15 路标准棋盘、双人对战、附带悔棋和认输逻辑的五子棋程序用 Pygame 做视觉层并不难难的是把胜负判定和回合状态机写得不糊。这份毕业设计论文里真正值钱的不是 Tkinter 画布上的那些create_oval而是作者拆出来的那一套feifei → xiaozhang → xiaoyuan/guixin判定链——每落一子只对当前颜色做四方向计数边界条件处理得相当干净。整套源码量不大但把 GUI 事件绑定、坐标吸附、回合切换、输赢提示这几个环节串成了一条完整的链路还附带黑盒加白盒的测试表非常适合做课程设计参考或者作为 Python 入门后第一个“完整项目”来拆。下面按我读源码时的实际顺序把这套程序从画布初始化到对局重置讲清楚。2. 棋盘坐标系设计与 Tkinter 画布组件的取舍2.1 为什么用 Tkinter 而不是 Pygame论文摘要里提到 Pygame但实际源码基于 Tkinter 实现这两者的技术选型差异值得先聊清楚。Pygame 的渲染循环是主动刷新式的每一帧都需要pygame.display.flip()推画面适合做实时动作类游戏而五子棋这种回合制棋类交互密度极低玩家一局可能只有几十次点击用事件驱动的 GUI 框架反而更省事。Tkinter 是 Python 标准库自带的 GUI 工具import tkinter之后直接能跑不需要额外安装这对要在实验室机器上演示、答辩前还要导出的毕业设计来说是实打实的便利。论文里把 Tkinter 写成 tkinker 属于笔误源码里是正确的import tkinter as tk。棋盘的核心数据结构是两组坐标列表这是整个程序的地基pieces_x [i for i in range(32, 523, 35)] pieces_y [i for i in range(38, 529, 35)]这两行代码生成的是 15 个等间距坐标点range(32, 523, 35)表示从 32 开始步长 35到 523 之前的整数为止。因为 32 35×14 522恰好小于 523所以生成 15 个点分别对应棋盘第 0 列到第 14 列。这个坐标系的物理含义是棋盘上的每个交叉点都被抽象为一个(x, y)元组玩家点击棋盘时程序把鼠标位置吸附到最近的交叉点上。如果步长和起始值不匹配比如步长改成 40那么同样的代码只能生成 13 个点棋盘就不是 15 路了。2.2 Canvas 画布上的棋子绘制与标签机制棋子的绘制逻辑集中在aaa函数中它接收一个颜色字符串black或white在全局坐标(click_x, click_y)处画一个圆def aaa(piece_color): global coor_black, coor_white canvas.create_oval(click_x - PIECE_SIZE, click_y - PIECE_SIZE, click_x PIECE_SIZE, click_y PIECE_SIZE, fill piece_color, tags (piece)) if piece_color white: coor_white.append((click_x, click_y)) elif piece_color black: coor_black.append((click_x, click_y)) feifei(piece_color)PIECE_SIZE 10所以棋子直径是 20 像素而棋盘交叉点间距是 35 像素棋子不会互相重叠。坐标对(click_x, click_y)被存入对应的全局列表这个列表同时承担两个角色一是后续胜负判定时遍历的棋子集合二是判断某个位置是否已有棋子、避免重复落子的依据。tags (piece)给所有棋子打上分组标签重置时一句canvas.delete(piece)就能清空全部棋子这个设计比逐个保存画布对象 ID 简洁得多。需要注意global coor_black, coor_white这里没有click_x和click_y因为这两个变量在aaa中没有赋值操作只做读取Python 会自动去全局作用域找。这是 Tkinter 事件回调里常见的写法——回调函数里直接改全局坐标而不是通过事件对象传参。2.3 点击事件的坐标吸附一个容易忽略的精度问题鼠标点击的原始坐标并不直接用于落子而是先经过yyy函数做一次“找最近交叉点”的处理。这个环节是提升棋盘手感的关键如果不做吸附玩家点击位置和实际落子位置偏移太明显会感觉棋子“钉不上去”def yyy(): global click_x, click_y coor coor_black coor_white global person_flag, show_piece item canvas.find_closest(click_x, click_y) tags_tuple canvas.gettags(item) if len(tags_tuple) 1: tags_list list(tags_tuple) coor_list tags_list[:2] try: for i in range(len(coor_list)): coor_list[i] int(coor_list[i]) except ValueError: pass else: coor_tuple tuple(coor_list) (click_x, click_y) coor_tuple这段代码的思路是在绘制棋盘时每个交叉点位置应该放置了带有坐标标签的隐藏对象源码中未完整展开这部分但这个逻辑是通过find_closest找到鼠标附近的对象再从对象标签中解析出对应的交叉点坐标。find_closest返回的是画布上离指定点最近的对象 IDgettags取到该对象的全部标签其中前两个标签被转换后作为交叉点坐标。把标签名当坐标存这种写法虽然不够优雅但在 Tkinter 里确实可行而且省去了单独维护一张坐标映射表的麻烦。这里有一个边界情况如果点击的空白区域没有任何可吸附对象find_closest会返回画布本身gettags拿到的是空元组len(tags_tuple) 1不成立落子操作直接被忽略。用户点击棋盘外区域不会误落子这个守卫条件值得保留。3. 落子的回合控制与状态切换机制3.1 事件绑定与鼠标响应入口整个程序的交互入口是一个绑定在画布上的鼠标事件def zsy(event): global click_x, click_y click_x event.x click_y event.y yyy()bind(Button-1, zsy)把鼠标左键点击事件绑定到zsy函数该绑定语句在源码中通过事件监听方式接入此处按回调结构实现。event.x和event.y是鼠标在画布上的相对坐标赋值给全局变量后调用yyy做吸附和落子判断。这个模式是 Tkinter 事件编程的标准套路事件对象只负责传递坐标业务逻辑放在被调用的普通函数里便于单独测试。3.2 黑棋只落一子的 bug 是怎么产生的论文第七部分记录的第一个问题是“落子一直只落黑棋没有白棋”这是典型的回合标志位未翻转导致的逻辑错误。源码中通过person_flag控制当前落子方if person_flag ! 0: if person_flag 1: aaa(black) zhangsiyuan(white) var.set(执白棋) elif person_flag -1: aaa(white) zhangsiyuan(black) var.set(执黑棋) person_flag * -1person_flag初始化为 1 表示黑方每次成功落子后乘-1翻转为-1再落一子又翻回 1这个写法比if person_flag 1: person_flag -1 else: person_flag 1简洁得多。出现“一直落黑棋”不是因为person_flag没有翻转而是因为在早期的测试版本中aaa函数内部没有把当前棋子颜色加入对应的coor_black或coor_white列表导致第二次点击时yyy里判断(click_x, click_y) not in coor永远成立看起来像可以随便落子但实际每次走的都是同一个分支。作者给出的解决思路是在aaa中新建一个piece_color参数每放置一枚棋子就按颜色分流记录坐标。这个做法让颜色数据流变得可追踪yyy根据person_flag决定调用aaa(black)还是aaa(white)aaa内部按颜色把坐标存到对应列表再调用feifei(piece_color)触发胜负判定。3.3 右侧棋子提示牌的同步更新zhangsiyuan函数负责更新界面右侧的“当前落子颜色”提示def zhangsiyuan(color): global piece_color piece_color color side_canvas.delete(show_piece) side_canvas.create_oval(110 - PIECE_SIZE, 25 - PIECE_SIZE, 110 PIECE_SIZE, 25 PIECE_SIZE, fill piece_color, tags (show_piece))先删除旧标签show_piece对应的棋子再在新位置画一个同大小的圆。fill piece_color直接用传入的颜色参数决定棋子明暗。这个方法必须在person_flag翻转之前调用否则提示的落子方会和实际落子方相差一个回合。源码中的顺序是先用aaa落子、再调用zhangsiyuan切换提示、最后person_flag * -1这个顺序保证了界面提示永远指向“下一步该谁走”。4. 胜负判定算法四方向分别计数的设计与边界处理4.1 三层函数架构导流、汇总、计数这套判定逻辑是全文最值得细读的部分。它没有用复杂的八方向统一循环而是拆成了三个层次feifei负责按颜色导流xiaozhang负责汇总横纵和对角线的判定结果xiaoyuan和guixin分别负责横纵轴和斜线方向的连续计数。先看导流层def feifei(piece_color): if piece_color black: xiaozhang(coor_black) elif piece_color white: xiaozhang(coor_white)feifei的入参是刚落下的棋子颜色它把对应颜色的完整坐标列表传给xiaozhang。这里有个设计细节判定时传的是整个颜色列表而不是单颗棋子坐标意味着每次落子后程序会对该颜色所有已落棋子做一次全量检查。15 路棋盘最多 225 个落子点黑白各占一半时每方约 112 颗棋子全量遍历的性能开销可以忽略但逻辑却变得非常简单——不用维护“最后落子位置”这一状态。4.2 横纵轴计数分两段数、只看连续xiaoyuan的横纵轴计数值得单独拆开看它把一条直线分成两段来数def xiaoyuan(coor): pieces_count 0 pieces_count yuan(coor, pieces_count, 1, 0) # 右边 pieces_count yuan(coor, pieces_count, -1, 0) # 左边 if pieces_count 4: return 1 else: pieces_count 0 pieces_count yuan(coor, pieces_count, 0, -1) # 上边 pieces_count yuan(coor, pieces_count, 0, 1) # 下边 if pieces_count 4: return 1 else: return 0pieces_count 4这个阈值意味着什么因为落下的那颗棋子本身已经占了一个位置只要在四个方向任一侧再数到 4 颗连续同色棋子加上自身就是五子连珠。yuan函数从当前坐标出发以 35 像素为步长沿指定方向逐一探测def yuan(coor, pieces_count, t1, t2): for i in range(1, 5): (x, y) (click_x t1 * 35 * i, click_y t2 * 35 * i) if (x, y) in coor: pieces_count 1 else: break return pieces_countrange(1, 5)限定最多向一个方向探测 4 步t1和t2是方向向量取值组合为(1,0)(-1,0)(0,-1)(0,1)(1,1)(-1,-1)(1,-1)(-1,1)正好覆盖四个正方向与四个斜方向。if (x, y) in coor这种写法依赖元组成员判断时间复杂度是 O(n)但对于百来颗棋子的规模完全够用。遇到空位就break不会跳过空位继续数。4.3 斜线方向两个 45 度方向的独立计数guixin处理的是两条对角线方向结构几乎和xiaoyuan一样只是方向向量不同def guixin(coor): pieces_count 0 pieces_count yuan(coor, pieces_count, 1, 1) # 右下角 pieces_count yuan(coor, pieces_count, -1, -1) # 左上角 if pieces_count 4: return 1 else: pieces_count 0 pieces_count yuan(coor, pieces_count, 1, -1) # 右上角 pieces_count yuan(coor, pieces_count, -1, 1) # 左下角 if pieces_count 4: return 1 else: return 0(1,1)和(-1,-1)是主对角线方向(1,-1)和(-1,1)是副对角线方向。这里没有做方向去重因为同一颗棋子的主对角线和副对角线是两条独立的线必须分别计数。如果棋子在某一侧数到 4加上落子本身构成一个活五或冲五判定胜利。4.4 汇总判定与游戏状态封锁xiaozhang汇总两个维度的结果def xiaozhang(coor): global person_flag, person_label if xiaoyuan(coor) 1 or guixin(coor) 1: siyuan() person_flag 0只要横纵轴或斜线任一方向达成五连就调用siyuan弹出胜负提示同时将person_flag置 0。person_flag 0是比赛结束的“总闸”后续所有落子操作因为if person_flag ! 0这一守卫条件全部失效。胜负提示函数def siyuan(): if person_flag -1: var1.set(白棋赢) elif person_flag 1: var1.set(黑棋赢) var2.set(游戏结束)这里person_flag的值仍然保留着胜利方的信息它为 1 时表示当前轮到的黑方赢了为 -1 时表示白方赢。有人会问落子后紧接着调用的是feifei(piece_color)此时person_flag还没翻转所以黑方落子时person_flag为 1白方落子时为 -1siyuan里读到的person_flag恰好就是胜利方的标志位。如果先翻转person_flag再判定这里的逻辑就要改成-person_flag作者没有踩这个坑顺序安排得很合理。4.5 一个隐藏的边界五子以上连珠的判定这版算法只做五连判定如果出现六连或更多pieces_count 4同样成立结果仍然是该颜色获胜。因为yuan最多向每个方向数 4 步六连的情况必然能数到 4判定通过。这在标准五子棋规则下是合理的——长连不判负的民间玩法很常见论文也没有引入禁手规则。如果需要做到专业规则下的黑棋禁手判定需要在yuan里增加长连检测但这份代码的定位是课程设计和毕业设计简化到五连即胜是符合预期的。5. 系统测试方案与典型异常定位思路5.1 功能测试矩阵的设计思路论文的系统测试部分采用黑盒加白盒的组合方案测试表按模块拆成了四张可视化模块、棋盘模块、玩家操作模块、胜负判定模块。可视化模块关注的是窗体能否正常弹出、窗口大小在切换应用后是否变化、按钮位置是否正确棋盘模块验证 15×15 规格和背景美观度玩家操作模块覆盖开始游戏、对局记录、设置三个入口胜负判定模块分别验证黑方胜、白方胜、平局三种终局状态。这个分层测试的思路可以直接借鉴到任何 GUI 项目里先验证“能不能打开、显示是否正常”再验证“操作入口是否响应”最后验证“核心业务逻辑是否正确”。实际执行时我一般会先跑一遍正常路径黑方胜场景再跑异常路径平局场景最后做回归测试确认修改判定算法不会影响落子功能。5.2 代码级测试的复现方式针对胜负判定这类纯逻辑函数脱离 GUI 直接测试会更高效。把xiaoyuan、guixin、yuan三个函数抽出来构造一组坐标列表手动验证# 构造一条水平五连坐标 coor_test [(32 35 * i, 38) for i in range(5)] # 模拟最后一颗棋子落在 (32 35 * 4, 38) click_x, click_y 32 35 * 4, 38 pieces_count 0 pieces_count yuan(coor_test, pieces_count, 1, 0) # 右侧无数 pieces_count yuan(coor_test, pieces_count, -1, 0) # 左侧数到4 assert pieces_count 4这里(32, 38)是棋盘的第一个交叉点(32 35*4, 38)是同一行的第五个交叉点。如果coor_test包含这五个点从第五颗向左数能数到 4 颗pieces_count为 4判定胜利。这个测试用例手工就能跑不需要启动 GUI 窗口。白盒测试的价值就在这里——你可以精确控制输入的坐标集合验证算法的每一步计算是否符合预期。5.3 棋盘边缘的越界问题论文源码中有一个容易被忽略的细节find_closest和坐标吸附逻辑在棋盘边缘的表现。当鼠标点击位置靠近棋盘第一行交叉点y 坐标为 38时向上按 35 像素步长探测会得到负数坐标但这个点在coor列表中必然不存在yuan的if (x, y) in coor会返回 False循环正常结束不会报错。也就是说坐标探测天然具备越界保护不需要额外判断棋盘边界。这个设计虽然依赖 Python 元组比较而不是显式边界检查但结果是对的也少写了一堆 if 语句。同样值得注意的还有平局判定。论文功能需求里提到“棋盘落满显示平局”但源码中并没有显式的平局检测逻辑——person_flag只可能被xiaozhang置 0如果棋盘落满仍然没有五连person_flag会持续在 1 和 -1 之间翻转没有任何代码会触发平局提示。这是一个需求与实现不一致的地方。如果在课程设计中需要实现平局可以在yyy的落子分支里加一个计数器当黑白棋子总数达到 225 且没有胜负时调用平局提示函数。论文测试表里写了平局测试用例通过可能是测试时模拟了棋盘落满的状态这部分在实际源码中并没有完整呈现。6. 悔棋功能扩展与 AI 判定的接口预留源码里定义了xiaofei作为重置按钮的回调负责清空棋盘和状态复位。如果要在此基础上加悔棋功能最直接的方案是利用coor_black和coor_white两个列表保存的落子顺序来撤销def regret_move(): global person_flag if person_flag 0: return if person_flag 1: # 当前轮到黑方说明刚才是白方落子 if not coor_white: return last_x, last_y coor_white.pop() else: if not coor_black: return last_x, last_y coor_black.pop() canvas.delete(piece) for x, y in coor_black: canvas.create_oval(x - PIECE_SIZE, y - PIECE_SIZE, x PIECE_SIZE, y PIECE_SIZE, fillblack, tags(piece)) for x, y in coor_white: canvas.create_oval(x - PIECE_SIZE, y - PIECE_SIZE, x PIECE_SIZE, y PIECE_SIZE, fillwhite, tags(piece)) person_flag * -1这个实现的关键是让coor_black和coor_white严格保持落子顺序因为append是尾部追加pop必然是撤销最近一步顺序天然正确。重绘所有棋子而不是只删最后一颗是为了避免 Tkinter 画布上删除单个对象后残留的层级缓存问题。如果你的项目要求“悔棋需要对方同意”可以在regret_move里先弹出一个messagebox.askyesno确认框确认后才执行撤销。如果还想给这个五子棋加上人机对战判定函数是最值得复用的部分。xiaozhang(coor)接收一个颜色坐标列表返回是否有五连这意味着 AI 可以遍历棋盘所有空位把空位加入己方列表后调用xiaozhang模拟落子如果返回 1 就说明这个位置能直接取胜。一层搜索的“逼近胜利”AI 只需要十几行代码就能接进来落子坐标依然是(x, y)元组和现有的coor_black、coor_white数据结构完全兼容。唯一需要改的是yyy中落子后不翻转person_flag改为调用 AI 搜索函数获取电脑落子位置执行aaa再翻转。这个接口预留是整套源码设计得最聪明的地方——判定逻辑和展示逻辑彻底分离做扩展时不需要动画布相关代码。本文还有配套的精品资源点击获取