简介:这是一份面向Python初学者与计算机相关专业学生的五子棋游戏源码,可作为毕业设计选题或课程实践项目,下载后即可运行体验双人对弈。资源包共10个文件,以5个py源码文件为主,辅以4个pyc编译文件与1个txt说明文档,整体仅9KB,体量轻巧便于阅读与二次修改。代码按界面、逻辑、工具等模块拆分,涵盖棋盘绘制、落子交互、五子连珠判定、胜负提示与重置等核心环节,并配有注释,方便理解面向对象编程与GUI设计思路。目前已有504人学习下载,适合想通过小项目巩固Python基础、锻炼算法与逻辑思维,或需要一份完整可运行毕业设计参考的读者,也可在此基础上扩展人机对战等进阶功能。
1. 双人对弈五子棋:从一份 Python 源码到能跑起来的毕业设计
很多人对「python五子棋游戏,可双人对弈玩耍,运行简单,项目可作为毕业设计,下载即可运行」这类标题的第一反应是:这不就是个练手小项目吗,能撑得起毕业设计?我一开始也这么想,直到帮学弟改过一版之后才发现,真正卡人的从来不是「五子棋规则有多难」,而是「下载即可运行」这五个字背后藏着的环境、依赖、棋盘坐标和胜负判定。双人对弈意味着没有 AI 搜索、没有强化学习,逻辑边界清晰,反而特别适合作为计算机毕业设计的起步骨架——你能把界面、事件循环、数据结构、胜负判定、悔棋重开这些模块拆得明明白白,论文里每一章都有东西可写。这篇笔记就按「一份能跑的双人对弈五子棋到底怎么落地」来讲,从环境准备到棋盘绘制、落子判定、再到打包交付,把参数和踩坑点都摊开。
2. 环境与依赖:让「下载即可运行」不翻车
2.1 为什么优先选 tkinter 而不是 pygame
双人对弈五子棋的核心诉求是「运行简单」,这一点直接决定了 GUI 库的选型。常见做法有两类:一类是 pygame,画面流畅、适合做动画和音效,但它需要额外 pip 安装,而且打包成 exe 时体积偏大;另一类是 tkinter,Python 标准库自带,装完 Python 就能 import,不需要任何第三方依赖。对于「下载即可运行」这个目标,tkinter 是更稳的选择——你不用担心对方机器上有没有装 pygame,也不用担心 pygame 和 Python 版本对不上导致pip install报错。
我一般会这样判断:如果项目要交毕业设计、要给导师演示、要发给同学试玩,选 tkinter;如果要做带 AI 搜索、带动画特效的进阶版,再考虑 pygame。双人对弈本身没有复杂渲染需求,tkinter 的 Canvas 完全够用。
2.2 用 conda 还是 venv 隔离环境
环境隔离这件事,新手最容易忽略,结果就是「我电脑上能跑,别人电脑上报 ModuleNotFoundError」。推荐用 venv,因为它是标准库自带,不需要额外装 conda:
# 创建虚拟环境,python 版本建议 3.8 及以上 python -m venv gomoku_env # Windows 激活 gomoku_env\Scripts\activate # macOS / Linux 激活 source gomoku_env/bin/activate # 确认当前 python 指向虚拟环境 python -c "import sys; print(sys.executable)"逻辑说明:python -m venv会在当前目录生成一个独立环境,所有后续 pip 安装都只影响这个目录,不会污染系统 Python。参数上,gomoku_env是环境名,可以随意改,但建议用英文小写加下划线,避免中文路径导致某些工具链解析失败。
激活之后,如果你用的是纯 tkinter 方案,其实不需要再装任何包。但为了代码规范和后续扩展,建议装两个辅助库:
pip install numpy pillownumpy 用来做棋盘状态的二维数组操作,pillow 用来在需要时加载棋子图片。注意:这两个库不是必须的,如果你的代码只用列表嵌套列表表示棋盘,numpy 可以省掉;如果棋子直接用 Canvas 画圆,pillow 也可以省掉。但毕业设计论文里如果写了「使用 numpy 进行矩阵运算」,那就得真的用上,别为了凑技术栈硬加。
2.3 版本兼容性检查清单
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| Python | 3.8 - 3.11 | 3.12 部分 tkinter 主题在 Windows 上有渲染差异 |
| tkinter | 随 Python 自带 | 不要单独 pip install tkinter,会失败 |
| numpy | 1.21+ | 仅当用矩阵表示棋盘时安装 |
| pillow | 9.0+ | 仅当加载图片资源时安装 |
| 操作系统 | Windows / macOS / Linux | tkinter 跨平台,但字体渲染略有不同 |
提示:如果你在 Linux 上运行,确认系统装了
python3-tk,否则import tkinter会报错。Ubuntu 下执行sudo apt-get install python3-tk即可。
3. 棋盘与落子:双人对弈的核心逻辑怎么写
3.1 用二维数组表示棋盘状态
五子棋棋盘常见规格是 15×15,也就是 225 个交叉点。用二维数组表示最直观:
# 0 表示空位,1 表示黑子,2 表示白子 BOARD_SIZE = 15 board = [[0] * BOARD_SIZE for _ in range(BOARD_SIZE)] def is_empty(row, col): """判断某个交叉点是否为空""" return board[row][col] == 0 def place_stone(row, col, player): """落子,player 为 1 或 2""" if not is_empty(row, col): return False board[row][col] = player return True逻辑说明:board是一个 15 行 15 列的列表,每个元素初始为 0。place_stone先检查是否为空,再写入玩家编号,返回布尔值表示落子是否成功。参数上,row和col都是从 0 开始的索引,对应棋盘上的交叉点。这里没有用 numpy,因为纯列表在 15×15 规模下性能完全够,而且不增加依赖。
3.2 胜负判定:四个方向扫描的边界处理
胜负判定是五子棋最容易写错的地方。核心思路是:每落一子,只检查经过这个点的四个方向(横、竖、左斜、右斜),看是否有连续五个同色棋子。
def check_win(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 < BOARD_SIZE and 0 <= c < BOARD_SIZE and board[r][c] == player: count += 1 r += dr c += dc # 反方向延伸 r, c = row - dr, col - dc while 0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE and board[r][c] == player: count += 1 r -= dr c -= dc if count >= 5: return True return False逻辑说明:directions定义了四个扫描方向,每个方向都从落子点向正反两侧延伸。count初始为 1,代表落下的这颗子本身。两个 while 循环分别向两侧走,遇到同色就加一,遇到边界或异色就停。参数上,dr和dc是行和列的变化量,比如(0, 1)表示横向向右,(1, 0)表示纵向向下,(1, 1)表示右下斜,(1, -1)表示左下斜。
这里有个血泪经验:边界检查必须放在数组访问之前,写成0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE and board[r][c] == player,利用 Python 的短路求值避免越界。如果你先访问board[r][c]再判断边界,程序会直接抛 IndexError。
3.3 用 Canvas 绘制棋盘和棋子
tkinter 的 Canvas 组件负责所有绘制工作。棋盘线、星位、棋子都画在同一个 Canvas 上:
import tkinter as tk CELL_SIZE = 40 MARGIN = 40 CANVAS_SIZE = CELL_SIZE * (BOARD_SIZE - 1) + MARGIN * 2 root = tk.Tk() root.title("五子棋 - 双人对弈") canvas = tk.Canvas(root, width=CANVAS_SIZE, height=CANVAS_SIZE, bg="#DEB887") canvas.pack() def draw_board(): """绘制 15x15 棋盘网格""" for i in range(BOARD_SIZE): # 横线 y = MARGIN + i * CELL_SIZE canvas.create_line(MARGIN, y, CANVAS_SIZE - MARGIN, y, fill="black") # 竖线 x = MARGIN + i * CELL_SIZE canvas.create_line(x, MARGIN, x, CANVAS_SIZE - MARGIN, fill="black") def draw_stone(row, col, player): """在指定交叉点画棋子""" x = MARGIN + col * CELL_SIZE y = MARGIN + row * CELL_SIZE r = CELL_SIZE // 2 - 2 color = "black" if player == 1 else "white" canvas.create_oval(x - r, y - r, x + r, y + r, fill=color, outline="gray")逻辑说明:CELL_SIZE是每个格子边长,MARGIN是棋盘边缘留白,CANVAS_SIZE是画布总尺寸。draw_board用循环画 15 条横线和 15 条竖线,形成网格。draw_stone根据行列算出像素坐标,用create_oval画圆。参数上,r是棋子半径,取CELL_SIZE // 2 - 2是为了让棋子之间留一点缝隙,视觉上更清晰。
3.4 鼠标点击事件与坐标转换
玩家点击画布时,需要把像素坐标转换成棋盘的行列索引:
def on_click(event): """处理鼠标点击,转换坐标并落子""" col = round((event.x - MARGIN) / CELL_SIZE) row = round((event.y - MARGIN) / CELL_SIZE) if not (0 <= row < BOARD_SIZE and 0 <= col < BOARD_SIZE): return global current_player if place_stone(row, col, current_player): draw_stone(row, col, current_player) if check_win(row, col, current_player): canvas.create_text(CANVAS_SIZE // 2, 20, text=f"玩家 {current_player} 获胜!", fill="red", font=("微软雅黑", 16)) canvas.unbind("<Button-1>") else: current_player = 3 - current_player # 切换 1 和 2 canvas.bind("<Button-1>", on_click)逻辑说明:round把像素坐标四舍五入到最近的交叉点。current_player用3 - current_player在 1 和 2 之间切换,这是个小技巧,比 if-else 简洁。获胜后canvas.unbind解绑点击事件,防止继续落子。参数上,event.x和event.y是 tkinter 自动传入的鼠标坐标,不需要手动获取。
4. 避坑与排查:双人对弈五子棋最常见的 5 个翻车点
4.1 落子位置偏移半个格子
现象:点击交叉点附近,棋子画在了格子中间而不是交叉点上。
原因:坐标转换时没有减去MARGIN,或者用了int而不是round。int会向下取整,导致点击右半边时仍然落在左边的交叉点。
解决:统一用round((event.x - MARGIN) / CELL_SIZE),并确保MARGIN和绘制棋盘时用的是同一个值。如果棋盘线画在MARGIN + i * CELL_SIZE,那么点击转换也必须减MARGIN。
4.2 胜负判定漏掉斜向五连
现象:横向和纵向五连能判出来,但斜向五连没反应。
原因:directions列表里只写了(0, 1)和(1, 0),漏掉了(1, 1)和(1, -1)。
解决:四个方向必须齐全。检查时可以用print把count打出来,确认每个方向都扫描到了。另外注意(1, -1)表示行增加、列减少,对应左下斜方向,别写成(-1, -1)导致重复扫描。
4.3 打包成 exe 后闪退
现象:用 PyInstaller 打包后,双击 exe 窗口一闪而过。
原因:tkinter 程序在打包时如果没有正确包含 tcl/tk 运行库,或者代码里有未捕获的异常,就会闪退。
解决:先用命令行运行 exe,看报错信息。常见做法是加--noconsole去掉黑窗口,但调试阶段先不加,保留控制台看输出。PyInstaller 命令参考:
pyinstaller -F -w --add-data ".;." gomoku.py参数说明:-F打包成单个文件,-w去掉控制台窗口,--add-data把当前目录资源一起打进去。如果程序不需要外部图片资源,--add-data可以省略。
4.4 中文标题或按钮显示乱码
现象:窗口标题、按钮文字显示成方块或问号。
原因:tkinter 默认字体在某些系统上不支持中文,或者源码文件编码不是 UTF-8。
解决:源码文件保存为 UTF-8,并在创建控件时显式指定中文字体,比如font=("微软雅黑", 12)。Windows 下微软雅黑基本都有,macOS 可以换成("PingFang SC", 12),Linux 用("WenQuanYi Micro Hei", 12)。
4.5 悔棋功能实现后棋盘状态错乱
现象:点了悔棋,棋子从界面上消失了,但再落子时判定逻辑不对。
原因:只删了 Canvas 上的图形对象,没有同步更新board二维数组。
解决:悔棋要同时做两件事——从board里把对应位置置 0,并从 Canvas 删除对应的 oval 对象。建议用一个栈记录每一步的(row, col, player, oval_id),悔棋时弹出栈顶,按记录恢复。oval_id 是create_oval的返回值,可以用来精确删除。
5. 从能跑到能交:毕业设计层面的扩展与验证
5.1 把双人对弈扩展成「人机对战」的最小改动
双人对弈的代码骨架已经包含了棋盘、落子、胜负判定,加 AI 只需要在玩家落子后插入一步电脑落子。最简单的 AI 是随机落子,进阶一点可以用评分表:对每个空位计算「如果下这里,我方得分多少、对方得分多少」,选最高分的位置。
import random def ai_move(player): """简单 AI:优先堵对方四连,否则随机""" empty = [(r, c) for r in range(BOARD_SIZE) for c in range(BOARD_SIZE) if board[r][c] == 0] if not empty: return None # 这里可以插入评分逻辑,先演示随机 return random.choice(empty)逻辑说明:empty收集所有空位,random.choice随机选一个。这个版本很弱,但足以验证「人机对战」的流程是否跑通。参数上,player是当前 AI 执子颜色,后续评分逻辑需要根据player和对手分别计算。
5.2 用单元测试验证胜负判定的正确性
毕业设计论文里如果写了「经过测试」,最好真的有测试代码。用 Python 自带的 unittest 写几个用例:
import unittest class TestGomoku(unittest.TestCase): def setUp(self): global board board = [[0] * BOARD_SIZE for _ in range(BOARD_SIZE)] def test_horizontal_win(self): for c in range(5): board[7][c] = 1 self.assertTrue(check_win(7, 4, 1)) def test_diagonal_win(self): for i in range(5): board[i][i] = 2 self.assertTrue(check_win(4, 4, 2)) def test_no_win(self): board[0][0] = 1 board[0][1] = 1 self.assertFalse(check_win(0, 1, 1)) if __name__ == "__main__": unittest.main()逻辑说明:setUp每个用例前重置棋盘,避免相互影响。test_horizontal_win在第七行前五列放黑子,检查(7, 4)是否获胜。test_diagonal_win在主对角线放白子。test_no_win只放两颗,确认不误判。这些用例跑通,论文里的「测试章节」就有实打实的内容。
5.3 交付前的检查清单
| 检查项 | 具体操作 | 通过标准 |
|---|---|---|
| 依赖 | 在干净虚拟环境里pip install -r requirements.txt | 无报错 |
| 运行 | 双击运行或python gomoku.py | 窗口正常弹出 |
| 对弈 | 黑白各走几步,故意凑五连 | 正确判胜 |
| 边界 | 点击棋盘四个角和边缘 | 不越界、不报错 |
| 打包 | PyInstaller 打包后在其他电脑运行 | 不闪退 |
| 文档 | README 写清 Python 版本和启动命令 | 新手能看懂 |
注意:requirements.txt 里不要写
tkinter,它不是 pip 包。如果用了 numpy 和 pillow,就写这两行即可。
5.4 一个我踩过的坑:别在循环里反复 bind
早期写事件处理时,我在每次落子后重新canvas.bind("<Button-1>", on_click),结果点击一次触发多次落子。原因是 bind 会叠加绑定,而不是替换。正确做法是在程序启动时 bind 一次,后续通过状态变量控制是否响应。这个坑很隐蔽,因为界面看起来正常,只是落子速度异常快,新手容易以为是鼠标问题。后来我养成习惯:所有事件绑定只在初始化阶段做一次,用self.game_over之类的标志位控制逻辑分支。
希望帮到你。
本文还有配套的精品资源,点击获取