☰
Python五子棋源码解析:人机对战与Minimax算法实战
2026/10/31 5:34:38 网站建设 项目流程

简介:这份Python五子棋源码面向Python初学者与进阶开发者,提供人机对战与人人对战两种模式的完整实现,可用于学习GUI编程、AI搜索算法与游戏逻辑设计。压缩包共4个文件,以3个py源码文件为主,另含1个pyc编译缓存文件,整体约9KB,体量轻巧便于快速阅读与二次修改。源码围绕棋盘绘制、鼠标事件响应、胜负判定等核心环节展开,人机模式中涉及Minimax或Alpha-Beta剪枝等搜索策略,人人模式则实现双玩家交替落子与合法性校验,代码按功能拆分模块,结构清晰。目前已有1890人学习下载,适合作为课程设计、练手项目或算法入门的参考实例,读者可从中掌握事件驱动编程、递归搜索优化与模块化组织思路,并在此基础上扩展难度、界面或AI强度。

1. 拆开这个压缩包:Python五子棋源码到底能跑出什么

很多人第一次搜「Python五子棋源码」,心里想的其实不是「我要学 Python」,而是「我想马上看到一个能落子的棋盘」。下载完Python五子棋源码(人机对战、人人对战).zip,解压出来通常是一堆.py文件加一张棋盘图,双击运行却可能只闪一下黑框就没了。问题不在源码,而在于你没先确认它依赖什么、入口在哪、人机对战的 AI 到底靠什么落子。

这份源码的价值,是把五子棋最核心的三件事一次性摊开:棋盘数据结构、胜负判定、以及一个能陪你下的人机搜索算法。人人对战让你验证规则,人机对战让你理解极小化极大(Minimax)配合评分函数是怎么工作的。适合刚学完 Python 基础语法、想找一个「有界面、有逻辑、能改」的练手项目的人,也适合想拿它当课程设计底稿的在校生。接下来我按「先跑通、再读懂、最后改得动」的顺序,把这份源码拆到你能自己复现的程度。

2. 跑通之前:环境、依赖与入口定位

2.1 先看清压缩包里的文件结构

拿到源码别急着运行,先列目录。常见的五子棋源码结构大致是这几类文件:主入口(main.py或gomoku.py)、棋盘与规则逻辑(board.py、rules.py)、AI 搜索(ai.py、minimax.py)、界面渲染(gui.py或直接用tkinter写在主文件里)、资源目录(images/、sounds/)。用一条命令把结构看清楚:

# 在解压后的目录里执行,只看两层,避免输出太长 find . -maxdepth 2 -type f | sort

逻辑说明:find的-maxdepth 2限制递归深度,防止资源文件夹里几百张图刷屏;-type f只列文件不列目录;sort让输出有序,方便你一眼找到main、ai、board这类关键词。参数上,如果你在 Windows 的 PowerShell 里,用Get-ChildItem -Recurse -Depth 2替代即可。

定位入口的判断标准很简单:找带if __name__ == "__main__":的那个文件,或者文件名里带main、run、start的。找到后先别改代码,直接跑。

2.2 依赖安装:tkinter、numpy 与 pygame 的分工

五子棋源码的依赖通常逃不出三个:tkinter(Python 自带,做界面)、numpy(棋盘矩阵运算)、pygame(部分源码用它做图形渲染和音效)。先确认 Python 版本,再按需装库:

# 确认版本,建议 3.8 及以上 python --version # tkinter 一般随 Python 自带,验证一下 python -c "import tkinter; print(tkinter.TkVersion)" # 如果源码用了 numpy 或 pygame,再装 pip install numpy pygame

逻辑说明:python -c "import tkinter"是快速验证法,能打印出版本号说明自带库没问题;如果报ModuleNotFoundError: No module named 'tkinter',那是 Linux 下没装python3-tk系统包,用sudo apt install python3-tk补上。pip install numpy pygame里,numpy 负责把棋盘当成二维数组做批量评分,pygame 负责把落子画成圆并播放音效。参数上,如果你用的是虚拟环境,先python -m venv venv再激活,避免污染全局环境。

提示:不要一上来就pip install -r requirements.txt,很多个人源码的 requirements 里版本号写得很死,反而装不上。先手动装 numpy 和 pygame 两个大头,缺什么补什么。

2.3 第一次运行:黑框闪退怎么定位

双击闪退是最高频的翻车现场。正确做法是在终端里运行,让报错留在屏幕上:

# 假设入口是 main.py,在终端里跑 python main.py

逻辑说明:终端运行会把Traceback完整打印出来,常见的有三类。第一类是FileNotFoundError,源码里写死了images/board.png这类相对路径,而你没在源码根目录下运行,解决方法是cd到入口文件所在目录再跑。第二类是ImportError,缺库,按 2.2 补装。第三类是AttributeError,多半是 Python 版本差异,比如老代码用了dict.has_key(),改成in即可。参数上,如果源码用了argparse,可以python main.py --help看它支持哪些启动参数,比如棋盘大小、先手方。

跑通的标准是:出现一个窗口,能看到棋盘网格,鼠标点击能落子,人人对战模式下黑白交替正常。到这一步,环境问题就全部清掉了。

3. 读懂棋盘与胜负判定:数据结构决定一切

3.1 用二维列表还是 numpy 数组

五子棋棋盘本质是一个 15×15 的矩阵,每个格子有三种状态:空、黑、白。源码里最常见的两种表示法,直接决定后面 AI 写起来顺不顺。

第一种是纯 Python 二维列表,用 0/1/2 表示:

# board[i][j]:0 空,1 黑,2 白 BOARD_SIZE = 15 board = [[0] * BOARD_SIZE for _ in range(BOARD_SIZE)] def place(board, row, col, player): if board[row][col] != 0: return False # 该点已有子,落子失败 board[row][col] = player return True

逻辑说明:[[0] * BOARD_SIZE for _ in range(BOARD_SIZE)]这种写法必须用列表推导,不能写成[[0]*15]*15,后者 15 行指向同一个列表对象,改一行全变,这是新手最经典的坑。place函数先判空再落子,返回布尔值让调用方知道是否成功。参数上,player约定 1 为黑、2 为白,后续胜负判定和 AI 评分都依赖这个约定。

第二种是 numpy 数组,好处是可以用切片和向量化做整行整列的胜负扫描:

import numpy as np board = np.zeros((15, 15), dtype=np.int8) def place_np(board, row, col, player): if board[row, col] != 0: return False board[row, col] = player return True

逻辑说明:dtype=np.int8把每个格子压到 1 字节,15×15 才 225 字节,内存几乎无感,但换来的是board[row, :]直接取整行、np.diagonal直接取对角线。参数上,如果你后面要写 AI 的批量评分,numpy 版本能省掉大量 for 循环。选型建议:纯练手、想看清每一步逻辑,用列表;想跑得快、后面要接搜索算法,用 numpy。

3.2 胜负判定的四个方向与边界处理

五子棋判定「五连」只需要检查刚落下的那颗子,沿四个方向各数一遍:横、竖、主对角线(左上到右下)、副对角线(右上到左下)。每个方向往正反两侧延伸,累计连续同色数达到 5 就赢。

def check_win(board, 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 < 15 and 0 <= c < 15 and board[r][c] == player: count += 1 r += dr c += dc # 反方向延伸 r, c = row - dr, col - dc while 0 <= r < 15 and 0 <= c < 15 and board[r][c] == player: count += 1 r -= dr c -= dc if count >= 5: return True return False

逻辑说明:directions里四个(dr, dc)覆盖了所有可能连成五子的方向,注意副对角线用(1, -1),行增列减。count初始为 1 是因为当前落子本身算一个。两个while分别向正反方向走,边界条件0 <= r < 15 and 0 <= c < 15必须写在前面,否则board[r][c]会先越界报错。参数上,count >= 5而不是== 5,是因为长连(六子及以上)在多数规则里也算赢,如果你要实现「恰好五子」的禁手规则,这里改成== 5并额外处理。

注意:判定函数只检查最后落子点,不要每次全盘扫描。全盘扫描在 15×15 上虽然不慢,但一旦 AI 要模拟成千上万步,这个开销会被放大几十倍。

3.3 人人对战的事件循环怎么接

人人对战是验证规则正确性的最小闭环。用 tkinter 的话,核心是把鼠标点击坐标换算成棋盘行列,再调用落子和判定:

import tkinter as tk CELL = 40 # 每格像素 MARGIN = 20 def on_click(event): col = round((event.x - MARGIN) / CELL) row = round((event.y - MARGIN) / CELL) if not (0 <= row < 15 and 0 <= col < 15): return global current_player if place(board, row, col, current_player): draw_piece(row, col, current_player) if check_win(board, row, col, current_player): show_winner(current_player) return current_player = 3 - current_player # 1 变 2,2 变 1

逻辑说明:round((event.x - MARGIN) / CELL)把像素坐标反算成格子索引,round比整除更宽容,点偏一点也能落准。3 - current_player是黑白切换的巧劲,1 和 2 互换不用写 if。place返回 False 时直接 return,保证已有子的格子不会被覆盖。参数上,CELL和MARGIN要和你的棋盘绘制代码保持一致,否则点击位置和视觉位置会对不上,这是界面类源码最常见的错位来源。

人人对战跑顺之后,规则层就算验证完毕,接下来才轮到人机对战。

4. 人机对战的核心:评分函数与极小化极大搜索

4.1 棋型评分:活四、冲四、活三怎么量化

AI 落子好不好,全看它怎么给一个局面打分。五子棋的评分函数通常按棋型给分,常见的一套分值如下表。这张表是无数五子棋 AI 调出来的经验值,你可以直接抄,也可以按自己手感微调。

棋型说明建议分值
连五五子连珠100000
活四两端开放的四连10000
冲四一端被封的四连1000
活三两端开放的三连1000
眠三一端被封的三连100
活二两端开放的二连100
眠二一端被封的二连10

评分函数的实现思路是:对每个空点,假设某方在此落子,然后沿四个方向统计形成的棋型,累加得分。下面是一个简化版:

SCORES = {'five': 100000, 'live_four': 10000, 'rush_four': 1000, 'live_three': 1000, 'sleep_three': 100, 'live_two': 100, 'sleep_two': 10} def evaluate_point(board, row, col, player): """假设 player 在 (row,col) 落子,返回该点的进攻分值""" total = 0 for dr, dc in [(0, 1), (1, 0), (1, 1), (1, -1)]: count = 1 blocked = 0 # 正方向 r, c = row + dr, col + dc while 0 <= r < 15 and 0 <= c < 15 and board[r][c] == player: count += 1 r += dr c += dc if not (0 <= r < 15 and 0 <= c < 15) or board[r][c] != 0: blocked += 1 # 反方向同理 r, c = row - dr, col - dc while 0 <= r < 15 and 0 <= c < 15 and board[r][c] == player: count += 1 r -= dr c -= dc if not (0 <= r < 15 and 0 <= c < 15) or board[r][c] != 0: blocked += 1 total += score_of(count, blocked) return total def score_of(count, blocked): if count >= 5: return SCORES['five'] if count == 4: return SCORES['live_four'] if blocked == 0 else SCORES['rush_four'] if count == 3: return SCORES['live_three'] if blocked == 0 else SCORES['sleep_three'] if count == 2: return SCORES['live_two'] if blocked == 0 else SCORES['sleep_two'] return 0

逻辑说明:evaluate_point对四个方向分别数连续同色子数count和被堵端数blocked,再交给score_of查表。blocked为 0 是活棋,为 1 是眠棋,为 2 基本没价值。参数上,SCORES里活三和冲四都给 1000,是因为实战中一个活三的威胁往往不亚于一个冲四,具体数值你可以按对局体验调。这套评分只算进攻,实际 AI 要同时算「我落这里的进攻分」和「对手落这里的进攻分」,后者代表防守价值。

4.2 Minimax 加 Alpha-Beta 剪枝的最小实现

有了评分,AI 要做的就是搜索。最朴素的思路是:我走一步,假设对手也走最优一步,我再走一步……往前看若干层,选对自己最有利的分支。这就是极小化极大(Minimax),加上 Alpha-Beta 剪枝能把无效分支砍掉大半。

def minimax(board, depth, alpha, beta, is_max): # 到达深度上限或分出胜负就返回评估值 if depth == 0: return evaluate_board(board) if is_max: best = -float('inf') for row, col in candidate_moves(board): board[row][col] = AI_PLAYER val = minimax(board, depth - 1, alpha, beta, False) board[row][col] = 0 best = max(best, val) alpha = max(alpha, best) if beta <= alpha: break # 剪枝 return best else: best = float('inf') for row, col in candidate_moves(board): board[row][col] = HUMAN_PLAYER val = minimax(board, depth - 1, alpha, beta, True) board[row][col] = 0 best = min(best, val) beta = min(beta, best) if beta <= alpha: break # 剪枝 return best

逻辑说明:is_max为 True 时是 AI 层,取最大值;为 False 时是对手层,取最小值。alpha是当前已知的最大下界,beta是最小上界,当beta <= alpha时说明这条分支不可能影响最终决策,直接break剪掉。board[row][col] = 0是回溯,必须写,否则棋盘会被搜索过程污染。参数上,depth一般设 2 到 4,设太深在纯 Python 里会卡到没法玩,后面 5.2 会讲怎么优化。

candidate_moves不要遍历全部 225 个空点,只取已有棋子周围两格内的空点,能把分支数从 200 多降到 20 以内,这是搜索能不能跑动的关键。

4.3 把 AI 接进界面:别让搜索卡死主线程

AI 搜索是计算密集型,如果直接在点击事件里同步调用,界面会假死几秒。常见做法是把搜索放到单独线程,算完再回主线程落子:

import threading def ai_move(): def work(): row, col = best_move(board, depth=3) # 回到主线程更新界面 root.after(0, lambda: apply_ai_move(row, col)) threading.Thread(target=work, daemon=True).start()

逻辑说明:threading.Thread把搜索丢到后台,daemon=True保证主窗口关闭时线程一起退出,不会残留。root.after(0, ...)是 tkinter 的线程安全更新方式,把落子动作排回主线程事件队列,直接在工作线程里操作界面控件会出各种玄学问题。参数上,depth=3是响应速度和棋力的折中,机器快可以上 4,机器慢就降到 2。

提示:如果源码里 AI 是同步的,你第一次点「人机对战」会感觉窗口卡住,别以为是死机,等几秒就好。想改造成异步,按上面这段套进去即可。

5. 避坑与排查:五子棋源码最常见的五个翻车点

5.1 棋盘画出来是歪的或点击错位

现象:棋盘网格能显示,但鼠标点下去落子位置和视觉位置差半格甚至一整格。原因:绘制时用的坐标公式和点击反算公式不一致,比如绘制用了MARGIN + col * CELL,反算却写成event.x / CELL忘了减MARGIN。解决:把绘制和反算的坐标公式抽成两个函数cell_to_pixel和pixel_to_cell,保证它们互为逆运算,改一处即可。

5.2 AI 第一步想太久或直接卡死

现象:人机对战开局,AI 迟迟不落子,甚至界面无响应。原因:搜索深度设太深,或者candidate_moves遍历了全部空点,分支爆炸。解决:把候选点限制在已有棋子周围两格内;深度先降到 2 验证能跑,再逐步加到 3;确认 Alpha-Beta 剪枝的break真的生效,可以在剪枝处加计数器打印剪掉了多少分支。

5.3 胜负判定漏判斜向五连

现象:横向竖向都能判赢,斜着连成五个却没反应。原因:directions里漏了(1, 1)或(1, -1),或者副对角线方向写成了(-1, -1)导致只往一个方向数。解决:四个方向一个都不能少,副对角线用(1, -1),并且正反两个while都要写,只写一个方向会漏掉「当前子在一端」的情况。

5.4 落子后棋盘数据被 AI 搜索改乱

现象:AI 思考完之后,棋盘上多出一些不该有的子,或者原有棋子消失。原因:Minimax 递归里落子后忘了回溯board[row][col] = 0,搜索过程把临时假设的棋子留在了真实棋盘上。解决:每一层递归落子后必须紧跟回溯,建议用try/finally包住,保证异常时也能还原。

5.5 打包成 exe 后图片和音效丢失

现象:源码里跑得好好的,用 PyInstaller 打包后一运行就报找不到images/board.png。原因:打包后资源路径变了,代码里的相对路径失效。解决:用sys._MEIPASS判断运行环境,写一个resource_path函数统一处理资源路径,打包时用--add-data把资源目录带进去。

6. 让 AI 从「能下」到「难缠」:三个我常调的技巧

第一个技巧是给评分函数加「双向威胁」权重。单纯累加单点分值,AI 会漏掉「同时形成两个活三」这种杀招。我的做法是在evaluate_board里统计全盘活三和冲四的数量,如果一方同时存在两个及以上活三,额外加一个大分值。这样 AI 会主动去制造双威胁,而不是只顾眼前一步。

第二个技巧是开局库。纯搜索的 AI 第一步往往下在天元附近,但缺乏变化。我一般会内置一个十几步的开局表,前两手直接查表落子,既省搜索时间,又让开局不那么呆。开局表不用大,覆盖常见的第一手、第二手应对即可。

第三个技巧是迭代加深。与其固定深度,不如从深度 1 开始搜,逐步加深,每次记录最优走法。这样在时间有限时,随时能拿到一个「当前深度下最好」的结果,而不是搜到一半被中断什么都没有。配合一个时间上限,AI 的响应就稳定了。

import time def iterative_deepening(board, max_depth, time_limit=2.0): start = time.time() best = None for depth in range(1, max_depth + 1): if time.time() - start > time_limit: break best = best_move(board, depth) return best

逻辑说明:iterative_deepening从浅到深逐层搜索,每层都更新best,一旦超时就返回上一层的结果。time_limit按你的耐心设,2 秒对普通玩家已经够用。参数上,max_depth给个上限防止无限加深,实际生效深度由时间决定。

我自己的习惯是:先把人人对战跑通确认规则无误,再单独拿 AI 模块做对局测试,最后才接界面。这样出问题时能快速定位是规则、搜索还是界面的锅。这套源码改到最后,你会发现真正难的不是写搜索,而是调评分——那才是决定 AI 棋风的地方。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询