资讯动态

Python五子棋双人对弈源码:pygame实现与毕业设计指南

发布时间:2026/10/26 15:56:58 来源:尧图企业网站定制
简介这是一份可直接运行的Python五子棋游戏源码面向计算机相关专业学生、Python初学者以及需要课程设计或毕业设计素材的开发者帮助解决从零搭建小游戏项目时缺少完整可参考代码的问题。资源包共10个文件以5个py源码文件为主辅以4个pyc编译文件与1个txt说明文档整体仅9KB轻量易携带。代码按模块拆分涵盖棋盘界面绘制、落子与胜负判断逻辑、工具函数及常量定义等部分结构清晰便于理解GUI与算法如何协作。已有504人学习下载说明其作为入门练手项目具有一定参考价值。读者可从中获得一套完整的双人对弈实现思路学习Tkinter界面搭建、五子连珠判定、非法落子拦截与棋局重置等常见处理方式并可直接运行体验或在此基础上扩展AI对战、悔棋等功能作为毕业设计或课程作业的起点较为合适。1. 从一份能跑的五子棋源码说起双人对弈到底难在哪很多人第一次做 Python 五子棋卡住的地方不是画不出棋盘而是落子之后怎么判断输赢、怎么保证两个人轮流下不会乱。标题里说的可双人对弈、运行简单、下载即可运行翻译成工程语言就是三件事一个能响应鼠标的棋盘界面、一套准确的五连判定逻辑、一个不依赖复杂环境就能启动的入口。这三件事拆开都不难难的是把它们拼成一个不用改配置、双击就能玩的完整程序。这篇文章面向两类人一类是刚学完 Python 基础、想拿五子棋练手的新手跟着步骤能自己敲出一个能玩的版本另一类是把五子棋当毕业设计选题的同学需要知道哪些地方能体现工作量、哪些参数和边界必须处理干净答辩时才不会被问倒。我会用 pygame 这条最稳的路线把棋盘绘制、事件循环、胜负判定、悔棋重开这些点一个个讲透中间穿插我实际写的时候踩过的坑。整套代码不依赖网络、不依赖数据库装好 pygame 就能跑这也是它适合当课程设计或毕业设计底座的原因。2. 用 pygame 搭出可交互棋盘从窗口到落子的最小闭环2.1 为什么选 pygame 而不是 tkinter做五子棋界面常见做法有三种tkinter 的 Canvas、pygame、以及 Web 前端。tkinter 是标准库不用装东西但它的绘图刷新和鼠标事件在高频重绘时容易卡顿棋盘线一多就闪。pygame 需要 pip 安装但它对每帧重绘整个画面这种游戏循环是原生支持的鼠标坐标换算、圆形棋子抗锯齿、双缓冲都省心。Web 前端体验最好但要引入浏览器和前后端通信对下载即可运行这个诉求是负担。所以选 pygame 的理由很直接单文件、单依赖、事件模型清晰。下面这段是最小可运行骨架先把窗口和棋盘网格画出来。import pygame import sys # 基础参数棋盘 15 路格子边长 40 像素留出边距 GRID_NUM 15 # 15x15 交叉点标准五子棋 CELL_SIZE 40 # 每个格子像素 MARGIN 40 # 棋盘四周留白 WIDTH CELL_SIZE * (GRID_NUM - 1) MARGIN * 2 HEIGHT WIDTH LINE_COLOR (0, 0, 0) BG_COLOR (238, 203, 140) # 木色棋盘 pygame.init() screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(五子棋 - 双人对弈) clock pygame.time.Clock() def draw_board(): screen.fill(BG_COLOR) # 画 15 条横线和 15 条竖线 for i in range(GRID_NUM): start MARGIN i * CELL_SIZE pygame.draw.line(screen, LINE_COLOR, (MARGIN, start), (WIDTH - MARGIN, start), 1) pygame.draw.line(screen, LINE_COLOR, (start, MARGIN), (start, HEIGHT - MARGIN), 1) pygame.display.flip() while True: for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() draw_board() clock.tick(60)这段代码的逻辑说明GRID_NUM决定棋盘路数标准五子棋是 15 路改成 19 就是 19 路棋盘但胜负判定里的连子数要跟着改。CELL_SIZE和MARGIN共同决定窗口尺寸WIDTH的算法是格子数减一乘以格宽再加两侧留白因为 15 个交叉点之间只有 14 段间隔这里算错就会出现棋盘偏左或右边多一截空白。clock.tick(60)把循环锁在 60 帧避免空转吃满 CPU。参数上最常调的是CELL_SIZE屏幕小就调到 30棋盘会紧凑想要大屏演示就调到 50。MARGIN建议不小于CELL_SIZE否则边缘的棋子会贴着窗口边视觉上很挤。2.2 鼠标坐标到棋盘交叉点的换算棋盘画出来了接下来要让点击落到最近的交叉点上。这里有个新手最容易翻车的地方鼠标点的是像素坐标棋盘存的是行列下标两者之间要做一次就近取整。换算公式是col round((x - MARGIN) / CELL_SIZE)row同理。用round而不是整除是因为玩家点的是交叉点附近取最近的才符合直觉。def pixel_to_grid(pos): x, y pos # 先减去边距再除以格宽四舍五入到最近交叉点 col round((x - MARGIN) / CELL_SIZE) row round((y - MARGIN) / CELL_SIZE) # 越界保护点到棋盘外直接丢弃 if 0 row GRID_NUM and 0 col GRID_NUM: return row, col return None def draw_stone(row, col, player): cx MARGIN col * CELL_SIZE cy MARGIN row * CELL_SIZE color (0, 0, 0) if player 1 else (255, 255, 255) pygame.draw.circle(screen, color, (cx, cy), CELL_SIZE // 2 - 2) # 白子加一圈黑边否则在木色棋盘上看不清 if player 2: pygame.draw.circle(screen, (0, 0, 0), (cx, cy), CELL_SIZE // 2 - 2, 1)逻辑说明pixel_to_grid返回None表示这次点击无效主循环里直接跳过不要让它去写棋盘数组否则会抛索引越界。draw_stone里白子描边这一步很多人会漏结果白棋落在浅色棋盘上几乎看不见答辩演示时很尴尬。棋子半径用CELL_SIZE // 2 - 2留 2 像素间隙避免相邻棋子粘在一起。到这里一个能画棋盘、能落子的交互闭环就成型了。下一章要解决的是核心问题怎么判断五连。3. 五连判定与对弈状态机把谁赢了算准3.1 四方向扫描的判定逻辑五子棋判定最朴素的做法是每次落子后以这个点为中心向四个方向横、竖、两条斜线各数一遍连续同色棋子任意方向达到 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 GRID_NUM and 0 c GRID_NUM and board[r][c] player: count 1 r dr c dc # 反方向延伸 r, c row - dr, col - dc while 0 r GRID_NUM and 0 c GRID_NUM and board[r][c] player: count 1 r - dr c - dc if count 5: return True return False逻辑说明directions里四个方向两两成对(0,1)是横向(1,0)是纵向(1,1)是从左上到右下(1,-1)是从右上到左下。每个方向都从落子点向正反两侧延伸count初始为 1 代表当前子本身。边界判断0 r GRID_NUM必须写在取值之前Python 的短路求值会保证越界时不会去访问board[r][c]。参数上要注意的是count 5而不是 5。标准规则里长连六子及以上通常也算赢用更稳妥。如果做的是有禁手规则的专业版本这里要改成精确判断但双人对弈的休闲版用就够了。3.2 双人对弈的状态管理双人对弈的状态其实很简单一个current_player变量在 1 和 2 之间切换一个board二维数组记录落子一个game_over标志控制是否还接受输入。但就是这几个变量管理不好就会出现赢了还能继续下同一点能重复落子这类问题。board [[0] * GRID_NUM for _ in range(GRID_NUM)] current_player 1 # 1 黑2 白 game_over False def handle_click(pos): global current_player, game_over if game_over: return grid pixel_to_grid(pos) if grid is None: return row, col grid if board[row][col] ! 0: # 该点已有棋子 return board[row][col] current_player draw_stone(row, col, current_player) pygame.display.flip() if check_win(board, row, col, current_player): game_over True print(f玩家 {current_player} 获胜) return current_player 3 - current_player # 1 变 22 变 1逻辑说明3 - current_player是切换玩家的常用技巧比写 if-else 简洁。board[row][col] ! 0这一句是防止重复落子的关键漏了它就会出现一颗子被覆盖的情况。game_over置位后所有点击直接返回保证赢棋后棋盘冻结。主循环里把handle_click接到鼠标事件上for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() elif event.type pygame.MOUSEBUTTONDOWN and event.button 1: handle_click(event.pos)event.button 1限定只响应左键右键可以用来做别的功能比如悔棋这个后面会讲。到这里一个完整的双人对弈逻辑就闭环了点击落子、切换玩家、判定胜负、冻结棋盘。4. 悔棋、重开与打包让程序真正下载即可运行4.1 用落子历史实现悔棋悔棋的本质是撤销上一步实现方式有两种一是把棋盘数组整个存快照二是维护一个落子历史栈。快照法简单但费内存历史栈法更优雅而且天然支持连续悔棋。history [] # 每项是 (row, col, player) def place_stone(row, col): global current_player board[row][col] current_player history.append((row, col, current_player)) draw_stone(row, col, current_player) pygame.display.flip() if check_win(board, row, col, current_player): return True current_player 3 - current_player return False def undo(): global current_player, game_over if not history: return row, col, player history.pop() board[row][col] 0 # 清空该点 current_player player # 把出子权还给刚才那个人 game_over False redraw_all() # 重绘整个棋盘逻辑说明undo里把current_player还原成被撤销那步的玩家这样悔棋后还是同一个人下符合直觉。redraw_all需要重画棋盘和所有历史棋子因为 pygame 不会自动擦除旧图形。def redraw_all(): draw_board() for r, c, p in history: draw_stone(r, c, p) pygame.display.flip()参数上history用列表当栈pop()从尾部取时间复杂度 O(1)。如果要做悔棋到任意步把列表换成支持索引的结构即可但双人对弈场景用单步悔棋就够了。4.2 重开一局与按键绑定重开就是把所有状态复位。这里有个坑board如果用[[0]*15]*15创建所有行会共享同一个列表引用改一行等于改所有行。必须用列表推导式。def restart(): global board, current_player, game_over, history board [[0] * GRID_NUM for _ in range(GRID_NUM)] # 不能写成 [[0]*15]*15 current_player 1 game_over False history [] redraw_all()按键绑定放在事件循环里elif event.type pygame.KEYDOWN: if event.key pygame.K_r: # R 键重开 restart() elif event.key pygame.K_u: # U 键悔棋 undo()[[0]*15]*15这个坑我见过太多人踩现象是落一颗子整列都出现棋子原因就是 15 个行引用指向同一个对象。列表推导式每次都新建一个列表才是正确写法。4.3 打包成可执行文件的注意事项下载即可运行如果对方没装 Python就需要打包。常见做法是用 PyInstallerpip install pyinstaller pyinstaller --onefile --windowed main.py--onefile打成单个 exe--windowed去掉黑色控制台窗口。打包后 exe 在dist目录里。要注意两点一是 pygame 打包后体积会到 30MB 以上这是正常的二是如果代码里用了外部图片或字体要用--add-data一起打进去否则运行时会报找不到文件。纯代码绘制的版本没有这个烦恼这也是我建议新手先用纯绘制的原因。5. 避坑与排查五子棋跑不起来时先看这几条5.1 落子位置整体偏移一格现象点击交叉点棋子却落在旁边。原因通常是pixel_to_grid里忘了减MARGIN或者用了整除//而不是round。解决确认公式是round((x - MARGIN) / CELL_SIZE)并且MARGIN和画线时用的是同一个值。如果画线用的是MARGIN i * CELL_SIZE换算就必须减MARGIN两边要对称。5.2 白棋看不见或棋子有锯齿现象白子落在木色棋盘上几乎隐形。原因白子颜色和背景对比度不够且没描边。解决给白子加一圈黑色轮廓pygame.draw.circle的最后一个参数传线宽 1 即可。另外pygame.draw.circle默认有抗锯齿如果觉得边缘糙可以先把画面画到更大的 Surface 再缩放但双人对弈场景没必要。5.3 赢了之后还能继续落子现象五连出现后点击棋盘还能下。原因check_win返回 True 后没有把game_over置位或者handle_click开头没检查game_over。解决两处都要有——判定赢时置位处理点击时先判断。这是状态机的完整性要求缺一不可。5.4 悔棋后棋盘出现重影现象悔棋后原来的棋子还在。原因pygame 是叠加绘制不清屏直接重画会留下旧图形。解决undo里必须调用redraw_all而redraw_all第一步是draw_board它会用背景色fill整个屏幕把旧内容盖掉。顺序不能反先清后画。5.5 打包后双击闪退现象PyInstaller 打出的 exe 双击一闪就没了。原因代码里有异常但--windowed把控制台藏了看不到报错。解决先用不带--windowed的命令打包一次在控制台里看报错信息定位到具体行再修。常见原因是路径问题或缺少依赖纯 pygame 代码一般不会遇到。6. 把五子棋做成毕业设计的加分项AI 对手与胜负统计如果只是双人对弈代码量大概两三百行作为毕业设计偏薄。想让它更有分量可以加两个方向一是接入一个简单的 AI 对手二是加胜负统计和落子记录。这两个都不难但能明显撑起工作量。AI 对手最省事的做法是评分表 贪心对棋盘上每个空点分别计算如果黑子下在这里、白子下在这里的分数取对我方最有利、对对方威胁最大的点。评分规则可以简化为连成 5 给 100000 分活四给 10000冲四给 1000活三给 1000活二给 100。这个评分表不需要机器学习纯规则就能让 AI 有模有样。def evaluate_point(board, row, col, player): score 0 directions [(0, 1), (1, 0), (1, 1), (1, -1)] for dr, dc in directions: count 1 # 统计正反两侧连续同色数量 for sign in (1, -1): r, c row dr * sign, col dc * sign while 0 r GRID_NUM and 0 c GRID_NUM and board[r][c] player: count 1 r dr * sign c dc * sign # 简单评分连子越多分越高 if count 5: score 100000 elif count 4: score 10000 elif count 3: score 1000 elif count 2: score 100 return score def ai_move(board, ai_player): best_score, best_pos -1, None for r in range(GRID_NUM): for c in range(GRID_NUM): if board[r][c] ! 0: continue # 进攻分 防守分对手下这里的分数 attack evaluate_point(board, r, c, ai_player) defend evaluate_point(board, r, c, 3 - ai_player) total attack defend * 0.8 # 防守权重略低 if total best_score: best_score, best_pos total, (r, c) return best_pos逻辑说明evaluate_point只数连续子没有区分活四和冲四属于简化版但对付休闲对局足够。ai_move遍历所有空点attack是 AI 自己下这里的价值defend是对手下这里的价值两者相加就是综合分。defend * 0.8让 AI 稍微偏向进攻不然它会一直堵人下起来很闷。这个权重可以调调到 1.0 就是完全攻守平衡调到 0.5 就是激进型。胜负统计更简单维护一个字典记录黑胜、白胜、平局次数每局结束时更新显示在窗口标题或侧边栏。落子记录则可以直接复用history列表导出成文本就是一份对局复盘。验证 AI 是否正常有个土办法让 AI 和自己下几局看它会不会在对方活三时去堵。如果它只顾自己连子不防守说明defend权重太低如果它每步都堵不进攻说明权重太高。我一般会调到 0.8 左右这个值下 AI 既有攻击性又不会漏防。最后说个习惯我写这类小项目一定会先把能跑的最小闭环跑通再往上加功能。棋盘能画、能落子、能判赢这三步没稳之前绝不碰 AI 和统计。很多人一上来就想做完整版结果卡在坐标换算上半天出不来热情就耗没了。先让程序跑起来再让它变好这个顺序别反。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑