简介:一款基于Python实现的五子棋小游戏源码,面向Python初学者与游戏开发爱好者,适用于课程设计、课后练手或图形界面编程入门。资源围绕五子棋核心玩法展开,涵盖棋盘初始化、棋子绘制、落子判定、胜负逻辑等完整环节,代码量精简且注释清晰,便于逐行阅读和二次修改。压缩包内共6个文件,包含3个Python脚本、2个pyc缓存文件以及1个exe可执行程序,整体大小约7.75MB;py文件负责游戏逻辑与交互,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, width=MARGIN * 2 + SIZE * (LINES - 1), height=MARGIN * 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 变 2,2 变 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", fill=color) 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 安装包默认内置 tkinter,Linux 环境则可能要执行
sudo apt install python3-tk才会出现tkinter模块。
运行这份代码后,你会看到一个能点棋子、黑白交替的棋盘,但谁赢没人管。下一步就是把胜负判定接进来,这是整个源码里最容易翻车的部分。
3. 落子与胜负判定:从“能下棋”到“判得准”
3.1 五连判定的四个方向:方向向量与双向扫描
五子棋胜负只有一条规则:横、竖、两条斜线,任一方向先形成连续五个同色棋子即获胜。听起来简单,但直接写最容易出两毛病:只扫了一个方向导致右侧连五判不出,或者数组越界直接抛异常。正确做法是以“刚落下的那颗棋子”为中心,沿四个方向各向两边扩散数数。
先看四个方向的方向向量。屏幕坐标系里,行号向下增大、列号向右增大,因此:
| 方向 | 行增量 dr | 列增量 dc | 对应棋形 |
|---|---|---|---|
| 水平 | 0 | 1 | 一行横着连 |
| 垂直 | 1 | 0 | 一列竖着连 |
| 主对角线 | 1 | 1 | 从左上到右下 |
| 副对角线 | 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(f"piece_{row}_{col}") self.current_player = player落子时每画一颗棋子都给它绑定唯一 tag:canvas.create_oval(..., tags=f"piece_{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 挡位之前,先把你自己的坐标换算独立成函数,这是这个源码后续扩展的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取