☰
Python五子棋源码:pygame双人对战与AI改造实战
2026/10/7 10:26:20 网站建设 项目流程

简介:这份 Python 五子棋小游戏源码面向刚接触 Python 图形界面与游戏逻辑的编程学习者,也适合作为课程设计、练手项目或教学演示的参考素材。压缩包共 6 个文件,以 3 个 py 源码文件为主,另含 2 个 pyc 编译文件与 1 个可直接运行的 exe 程序,整体约 7.75MB,源码与可执行文件并存,便于对照阅读与快速体验。内容围绕棋盘绘制、落子判定、胜负检测等核心模块展开,从预览可见 checkerboard、Gomoku 等模块划分,结构清晰,适合逐层拆解五子棋的界面刷新、坐标映射与连珠判断思路。已有 375 人学习下载,可作为入门 Python 游戏开发、理解事件循环与二维数组应用的实践样本,也能在此基础上扩展悔棋、人机对战等功能。

1. 五子棋 Python 源码:一个能跑、能改、能交作业的完整小游戏

如果你正在搜「Python 游戏源码」,大概率不是想从零学 pygame 文档,而是想找一个能直接跑起来、代码结构清楚、方便改的小项目。这份五子棋源码就是干这个的:一个基于 pygame 的本地双人对战五子棋,包含棋盘绘制、落子判定、五连胜负检测、悔棋和重新开始。它不依赖网络、不依赖数据库,装好 Python 和 pygame 就能运行,适合刚学完 Python 基础、想拿一个完整项目练手的人,也适合需要交课程设计、想快速看懂一个游戏主循环怎么写的同学。下面我按「拿到源码后怎么跑通、代码怎么组织、参数怎么调、哪里容易翻车」的顺序拆一遍,你照着走基本不会卡住。

2. 环境准备与源码结构:从解压到第一次落子

2.1 运行前必须确认的三件事

五子棋源码本身不复杂,但它对运行环境有硬性要求,装错了就是黑屏或者直接报 ModuleNotFoundError。我一般拿到任何 Python 游戏源码,先确认三件事:Python 版本、pygame 是否安装、源码目录结构是否完整。

Python 版本建议 3.8 及以上。3.8 是很多教程和库的兼容基线,源码里用到的 f-string、类型注解在 3.6 之后都支持,但 3.8 更稳。查看版本:

python --version # 或者在某些系统上 python3 --version

如果输出是 Python 3.8.x 以上,继续。如果提示命令不存在,说明 Python 没装或者没加进环境变量,先去官网下载安装包,安装时勾选「Add Python to PATH」,这一步漏了后面全是坑。

pygame 是这份源码唯一的外部依赖。安装命令:

pip install pygame

装完验证一下,能打印版本号才算成功:

import pygame print(pygame.version.ver)

如果 pip 下载慢或者报错,常见做法是换国内镜像源,比如清华源:

pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple

源码目录一般长这样:主程序文件(比如gomoku.py或main.py)、可能有一个images或assets文件夹放棋子图片、一个README。如果只有单个.py文件,那更简单,直接进目录运行即可。运行命令:

python gomoku.py

窗口弹出来、棋盘画出来,就说明环境这关过了。

2.2 源码的模块划分与主循环逻辑

这份五子棋源码虽然不大,但结构是完整的,通常分成四块:常量配置、棋盘数据结构、绘制函数、事件主循环。看懂这四块,你就能改任何地方。

常量配置一般在文件开头,定义棋盘格数、格子像素大小、窗口宽高、颜色值。比如:

BOARD_SIZE = 15 # 15x15 标准五子棋棋盘 CELL_SIZE = 40 # 每个格子 40 像素 MARGIN = 40 # 棋盘四周留白 WINDOW_SIZE = BOARD_SIZE * CELL_SIZE + MARGIN * 2 BLACK = (0, 0, 0) WHITE = (255, 255, 255) LINE_COLOR = (0, 0, 0)

BOARD_SIZE决定棋盘是 15 路还是 19 路,CELL_SIZE决定窗口大小和点击精度。这两个参数一改,整个界面尺寸跟着变,后面绘制和坐标换算都依赖它们,所以改的时候要一起改,不能只动一个。

棋盘数据结构通常是一个二维列表:

board = [[0] * BOARD_SIZE for _ in range(BOARD_SIZE)] # 0 表示空,1 表示黑子,2 表示白子

用 0/1/2 而不是字符串,是为了判定胜负时做数值比较更快,也方便扩展 AI。落子就是把对应位置改成 1 或 2。

绘制函数负责把二维数组画成屏幕上的棋盘和棋子。核心是把数组下标 (row, col) 换算成屏幕像素坐标:

def board_to_pixel(row, col): x = MARGIN + col * CELL_SIZE y = MARGIN + row * CELL_SIZE return x, y

这个换算关系是整份源码的骨架,点击落子时反向换算,绘制棋子时正向换算,两边必须一致,否则就会出现「点这里、子下到那里」的经典 bug。

主循环是 pygame 程序的标准写法:

running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.MOUSEBUTTONDOWN: x, y = event.pos col = round((x - MARGIN) / CELL_SIZE) row = round((y - MARGIN) / CELL_SIZE) if 0 <= row < BOARD_SIZE and 0 <= col < BOARD_SIZE: if board[row][col] == 0: board[row][col] = current_player if check_win(row, col, current_player): print("玩家", current_player, "获胜") running = False current_player = 3 - current_player # 1 和 2 互换 draw_board() pygame.display.flip()

pygame.event.get()一次取走所有事件,MOUSEBUTTONDOWN里拿到的event.pos是窗口像素坐标,减掉边距再除以格子大小、四舍五入,就得到最近的交叉点行列号。round而不是int,是因为玩家点击很难正好落在交叉点上,四舍五入能吸附到最近的点,手感更好。current_player = 3 - current_player是个小技巧,1 变 2、2 变 1,比写 if-else 简洁。

胜负检测check_win是这份源码里最值得读的函数,它从当前落子点出发,沿四个方向数连续同色棋子:横、竖、左斜、右斜。每个方向往两边各数一次,加起来够 5 就赢。这个逻辑后面第 4 章会展开讲,因为它是改 AI 和对战规则的基础。

3. 核心机制拆解:落子、胜负判定与悔棋实现

3.1 胜负检测的四个方向与边界处理

五子棋判定胜负,本质是问:刚下的这颗子,在横、竖、两条斜线上,有没有形成连续五个同色。实现时最容易翻车的地方是数组越界和方向数重复。

常见写法是定义一个方向列表:

DIRECTIONS = [(0, 1), (1, 0), (1, 1), (1, -1)] # 分别代表:水平、垂直、主对角线、副对角线

对每个方向,从当前点向正负两个方向延伸计数:

def check_win(row, col, player): 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

while条件里同时判断边界和颜色,顺序不能反。如果先访问board[r][c]再判断边界,当 r 或 c 越界时 Python 会抛 IndexError,程序直接崩。把边界判断放前面,利用短路求值,越界时后面的颜色比较根本不会执行。

count >= 5而不是== 5,是因为标准五子棋里长连(六连及以上)也算赢。如果你要做「正好五连才算赢」的变体规则,这里改成== 5即可,但要注意长连情况下会漏判,需要额外处理。

方向列表只写四个而不是八个,是因为每个方向的正负两边都数了,写八个会重复计数。这是新手改代码时最常见的错误:加了八个方向,结果三颗子就被判成六连。

3.2 悔棋功能的数据结构设计

悔棋看起来简单,做起来要考虑「悔一步还是悔两步」。双人对战里,点一次悔棋通常应该退回上一步,让当前玩家重新下。实现方式是用一个栈记录落子历史:

history = [] # 每个元素是 (row, col, player) # 落子时 board[row][col] = current_player history.append((row, col, current_player)) # 悔棋时 if history: r, c, p = history.pop() board[r][c] = 0 current_player = p # 把出棋权还给刚才那个人

用列表当栈,append入栈、pop出栈,天然后进先出。悔棋时把棋盘对应位置清 0,再把current_player设回被撤销的那一手,这样界面和状态就一致了。

这里有个容易忽略的点:悔棋后要重绘棋盘,否则屏幕上还留着那颗子,但数据已经没了,下次点击同一位置会出问题。所以悔棋处理完必须调一次draw_board()。

如果要做「悔两步」(撤销自己和对方各一手),就连续 pop 两次,但要判断 history 长度够不够,否则空栈 pop 会报 IndexError。常见做法是:

if len(history) >= 2: for _ in range(2): r, c, p = history.pop() board[r][c] = 0 current_player = history[-1][2] if history else 1

最后一行是恢复当前该谁下,逻辑是「看历史里最后一手是谁下的,下一手就轮到对方」,如果历史空了就默认黑棋先手。

3.3 重新开始与状态重置

重新开始不是简单清空棋盘就完事,要把所有相关状态一起重置:棋盘数组、落子历史、当前玩家、游戏结束标志。漏掉任何一个都会出现「棋盘清了但轮到白棋先下」或者「赢了之后重开还是判赢」的玄学问题。

def restart(): global board, history, current_player, game_over board = [[0] * BOARD_SIZE for _ in range(BOARD_SIZE)] history = [] current_player = 1 game_over = False

注意board要用列表推导重新生成,不能写board = [[0] * BOARD_SIZE] * BOARD_SIZE。后者生成的是同一行的引用,改一行全变,这是 Python 二维数组的经典坑,血泪经验。判断方法很简单:改board[0][0],如果board[1][0]也跟着变,就是用错了。

game_over标志用来在胜负已分后屏蔽落子事件,否则玩家还能继续下,把已经结束的棋局搞乱。在主循环的点击处理里加一句if game_over: continue就行。

4. 避坑与常见问题排查:跑不起来先看这几条

4.1 窗口一闪而过或直接退出

现象:双击运行或命令行执行后,窗口闪一下就没了,没有任何报错。

原因:主循环没有正确保持,或者pygame.quit()和sys.exit()被提前调用。常见于把事件循环写在函数里但忘了调用,或者while条件一开始就是 False。

解决:确认主循环是while running:且running初始为 True;确认pygame.init()在创建窗口之前调用;如果用了sys.exit(),检查它是不是被放在了循环外面。可以在pygame.quit()前加一句print("exit"),看是否真的走到了退出逻辑。

4.2 点击落子位置偏移

现象:鼠标点在交叉点上,棋子却下到了旁边一格,越往棋盘边缘偏得越厉害。

原因:坐标换算公式和绘制公式不一致,或者MARGIN、CELL_SIZE在两个地方用了不同的值。常见于绘制时用了硬编码数字,而点击换算用了常量。

解决:把坐标换算抽成一个函数,绘制和点击都调它,保证只有一处定义。检查round和int的使用:点击吸附应该用round,如果用int直接截断,靠近格子左边和上边的点击会偏到前一格。

4.3 胜负判定误报或漏报

现象:明明只有四颗子却判赢,或者五颗子连成一线却没反应。

原因:方向数重复导致 count 虚高,或者边界判断顺序错误导致漏数了边缘的棋子。漏报还可能是check_win只在落子后调用,但落子位置传错了行列。

解决:打印每次check_win的count值,看四个方向分别数了多少。如果某个方向明显偏大,检查方向列表是不是写成了八个。边缘漏报就检查while条件里边界和颜色的顺序,确保先判边界。另外确认调用时传的是row, col而不是col, row,行列颠倒会让判定完全错位。

4.4 pygame 安装成功但 import 报错

现象:pip install pygame显示成功,但import pygame提示找不到模块。

原因:机器上有多个 Python 版本,pip 装到了 A 版本,运行用的是 B 版本。或者虚拟环境没激活。

解决:用python -m pip install pygame代替直接pip install,这样能保证装到当前python对应的环境里。然后用python -c "import pygame; print(pygame.__file__)"确认模块路径。如果用了 venv 或 conda,先激活环境再装。

4.5 中文显示成方块

现象:如果源码里有中文提示文字,窗口里显示成一个个方块。

原因:pygame 默认字体不支持中文。

解决:加载系统里的中文字体,比如 Windows 下的simhei.ttf或msyh.ttc:

font = pygame.font.Font("C:/Windows/Fonts/simhei.ttf", 24)

或者用pygame.font.SysFont("simhei", 24)。路径写错会直接报错,确认字体文件存在。跨平台的话,把字体文件放进源码目录一起分发更稳。

5. 进阶改造:把双人对战改成带 AI 的版本

5.1 用评分函数给每个空位打分

双人对战跑通之后,很多人想加个电脑对手。最直接的做法不是上深度学习,而是给每个空位算一个分,选最高的落子。评分逻辑就是:假设我在这里下,能形成几连;同时假设对手在这里下,能形成几连,两者加权。

def evaluate_point(row, col, player): score = 0 for dr, dc in DIRECTIONS: count = 1 # 数自己这边 for direction in (1, -1): r, c = row + dr * direction, col + dc * direction while 0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE and board[r][c] == player: count += 1 r += dr * direction c += dc * direction score += count ** 2 # 连得越多分越高,平方拉开差距 return score def ai_move(player): opponent = 3 - player best_score = -1 best_pos = None for r in range(BOARD_SIZE): for c in range(BOARD_SIZE): if board[r][c] == 0: attack = evaluate_point(r, c, player) defend = evaluate_point(r, c, opponent) total = attack * 1.2 + defend # 进攻权重略高 if total > best_score: best_score = total best_pos = (r, c) return best_pos

count ** 2是让四连的分远高于三连,逼 AI 优先堵活四。attack * 1.2 + defend里的 1.2 是进攻系数,调大 AI 更激进,调小更保守。这个系数没有标准答案,我一般从 1.0 到 1.5 之间试,看对局手感。

这个评分 AI 的边界很明显:它只看单点局部,不会算两步之后的棋,遇到双三、冲四活三这种组合会漏。但对一个练手项目来说够用,而且代码短、好懂、好改。

5.2 用极小化极大搜索提升棋力

如果嫌评分 AI 太弱,可以加一层搜索。极小化极大(Minimax)的思路是:我下完之后,假设对手也选最优应对,往前看 N 步,选一个最坏情况下最好的落子。配合 Alpha-Beta 剪枝,深度 3 到 4 层在 15 路棋盘上还能接受。

def minimax(depth, is_max, alpha, beta): if depth == 0 or game_over: return evaluate_board() if is_max: best = -float("inf") for r, c in get_empty_points(): board[r][c] = ai_player val = minimax(depth - 1, False, alpha, beta) board[r][c] = 0 best = max(best, val) alpha = max(alpha, best) if beta <= alpha: break return best else: best = float("inf") for r, c in get_empty_points(): board[r][c] = human_player val = minimax(depth - 1, True, alpha, beta) board[r][c] = 0 best = min(best, val) beta = min(beta, best) if beta <= alpha: break return best

get_empty_points不要返回全部空位,只返回已有棋子周围两格内的空位,否则搜索空间爆炸,深度 3 都要算很久。这是实战里必须做的剪枝,不然程序会卡到你以为死机了。

evaluate_board是整盘棋的评分,可以简单地把所有己方棋子的evaluate_point加起来减去对方的。搜索深度每加一层,耗时大概翻几倍,深度 4 在普通笔记本上单步可能要一两秒,体验会变差,所以深度和评分函数的复杂度要平衡。

5.3 改造时的验证方法

改完 AI 别急着说「能跑就行」,至少验证三件事:AI 会不会下在已经有子的位置(说明空位判断漏了)、AI 面对活四会不会堵(说明防守权重不够)、AI 自己有成五机会时会不会抓(说明进攻评分没生效)。

我一般会手动摆几个固定局面:给 AI 一个冲四,看它堵不堵;给它一个活三,看它理不理;给它自己一个四连,看它会不会直接成五。这三个场景过了,基本逻辑就没大问题。剩下的棋力强弱,靠多下几盘调系数,没有捷径。

从那以后我每次拿到一份游戏源码,都强制先跑通默认配置、再动一行代码,确认基线能复现之后才开始改。这样出了问题能立刻分清是源码本身的坑还是我改出来的坑。希望这份拆解帮到你,源码拿去跑一遍,比看十篇教程都实在。

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

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

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

立即咨询