ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python五子棋人机对战:从棋盘建模到AI评分算法实战

Python五子棋人机对战:从棋盘建模到AI评分算法实战 简介面向Python编程与数据分析课程的结课报告场景五子棋对弈的算法设计资料包将算法思路与编码实现完整串联适合正在准备课程设计或结课报告的学生参考。报告部分按程序思路介绍、设计方案、源程序代码、程序运行及结语五章逐层展开图文表并茂逻辑清晰可帮助快速梳理从问题分析到结果呈现的完整过程代码部分包含4个Python文件分别负责棋盘绘制、人人对战与人机对战等核心模块结构独立、可直接运行也便于替换或扩展策略后二次练习。资料包共5个文件以py脚本和docx文档为主压缩包大小仅1.08MB轻量易用内部按“报告”与“代码”分区存放查找使用十分方便。目前已有1831人学习下载适合作为Python编程与数据分析相关课程的结课报告范例也可作为五子棋算法入门、人机策略设计和小型游戏项目实践的参考材料。1. 五子棋人机对战一个把语法、算法和数据分析串起来的结课项目很多Python基础课结束时的作业都停在增删改查或爬虫但这个五子棋对弈项目不太一样它要同时处理棋盘的状态表示、人机交互的事件循环、AI决策的评分逻辑还要把整个过程写进一份像样的结课报告。整个代码量不算大四个Python文件加起来也就几百行但你把它跑起来后会发现自己其实已经在用“数据分析”的思维了——把一局对弈拆成特征、评估、决策最后还能用图表说明为什么AI会在某一手选择那个位置。这个资源里既有完整源码也有Word报告适合那些想把期末作业做得有点工程感的人。2. 棋盘建模与程序模块拆分先定义状态再谈算法五子棋的对弈逻辑在纸面上很简单但落到代码里第一个关键问题是棋盘怎么表示才算既容易判断胜负又能快速计算AI的落子点。最常见的做法是用一个15×15的二维数组0表示空位1表示黑子2表示白子。这个方案没有花哨的对象封装但胜在直白访问任意位置都是O(1)开销。2.1 用二维数组保存棋盘状态# checkerboard.py SIZE 15 # 棋盘15x15 def init_board(): 初始化空棋盘, 返回15x15的二维列表 return [[0 for _ in range(SIZE)] for _ in range(SIZE)] def is_valid_move(board, x, y): 判断(x, y)是否可落子: 坐标合法且为空 if not (0 x SIZE and 0 y SIZE): return False return board[x][y] 0 def place_piece(board, x, y, player): 落子并返回新棋盘, 不直接修改原对象 new_board [row[:] for row in board] new_board[x][y] player return new_board这里我刻意让place_piece返回新棋盘而不是原地修改这是为了后面做AI搜索时方便回溯。如果只是人人对战直接board[x][y] player就够了但人机对战的多步计算会需要临时状态提前用复制方式能省去恢复现场的麻烦。player取值1或2对应黑子和白子。2.2 四个Python文件怎么分工资源包里的四个文件不是随手扔在一起它们刚好对应了程序的三层职责。checkerboard.py只负责棋盘数据的初始化和基础判断相当于数据层。ManAndMan.py把数据层和pygame的绘制、鼠标事件撮合在一起完成人人对战。ManAndMachine.py在ManAndMan.py的基础上引入AI落子函数相当于把决策层塞进事件循环。另外一个文件名带“五子棋【人机对战】”的脚本通常保存了AI的具体算法实现有时会和ManAndMachine.py合并取决于当初写报告时的拆分习惯。模块边界清晰有个很实际的好处结课报告里写“程序结构设计”章节时直接画一张表把四个文件对应到功能就非常清楚。文件职责依赖checkerboard.py棋盘初始化、合法落子判断、胜负检测的数据支撑无ManAndMan.py人人对战主循环、pygame画面、鼠标落子checkerboardManAndMachine.py人机对战主循环、调用AI接口、显示AI落子checkerboard, AI函数五子棋【人机对战】.pyAI评估与选点算法checkerboard这里多说一句把AI算法单独放一个文件能避免以后你想给AI加“难度等级”时去主循环里翻逻辑。我一般会在项目里再建一个ai.py但这个资源里既然已经拆出单独文件就保持原样报告里也好解释。3. pygame棋盘绘制与人人对战事件循环棋盘画在屏幕上不是简单画线要处理坐标换算和窗口尺寸的对应关系。常见做法是把窗口设为640×640棋盘每条线间隔40像素留20像素边距这样15条线刚好铺满。每一个交叉点就是落子位置鼠标点击时需要把像素坐标反向换算成棋盘坐标。3.1 坐标换算与棋子绘制import pygame from checkerboard import SIZE CELL 40 MARGIN 20 WIDTH HEIGHT CELL * (SIZE - 1) MARGIN * 2 def screen_to_board(pos): x, y pos col round((x - MARGIN) / CELL) row round((y - MARGIN) / CELL) return row, col def draw_board(screen, board): screen.fill((210, 180, 140)) # 木色背景 for i in range(SIZE): start MARGIN i * CELL pygame.draw.line(screen, (0, 0, 0), (MARGIN, start), (MARGIN CELL * (SIZE - 1), start), 1) pygame.draw.line(screen, (0, 0, 0), (start, MARGIN), (start, MARGIN CELL * (SIZE - 1)), 1) for r in range(SIZE): for c in range(SIZE): if board[r][c] 1: pos (MARGIN c * CELL, MARGIN r * CELL) pygame.draw.circle(screen, (0, 0, 0), pos, 17, 0) elif board[r][c] 2: pos (MARGIN c * CELL, MARGIN r * CELL) pygame.draw.circle(screen, (255, 255, 255), pos, 17, 0)screen_to_board里用了round而不是int这样才能保证点击交叉点附近的像素都能落到最近的格子上。如果你用int做强制转换点击位置在棋盘左侧时会因为浮点误差出现坐标偏一格的问题。棋子半径设为17像素比半个格子略小黑子和白子之间留一点视觉空隙。3.2 事件循环里的回合切换人人对战的主循环并不复杂但容易在“落子后切换玩家”这里出错。下面的代码片段展示了最小可用逻辑# ManAndMan.py import pygame from checkerboard import init_board, is_valid_move, place_piece def main(): pygame.init() screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(五子棋 - 人人对战) board init_board() current_player 1 # 1黑, 2白 clock pygame.time.Clock() running True while running: clock.tick(30) for event in pygame.event.get(): if event.type pygame.QUIT: running False if event.type pygame.MOUSEBUTTONDOWN and event.button 1: r, c screen_to_board(event.pos) if is_valid_move(board, r, c): board place_piece(board, r, c, current_player) current_player 3 - current_player # 1变2, 2变1 draw_board(screen, board) pygame.display.flip() pygame.quit()current_player 3 - current_player是回合切换的小技巧比current_player 2 if current_player 1 else 1简洁。注意place_piece返回新棋盘所以主循环里要重新赋值给board否则棋盘数组不会更新。这个坑我见过很多同事踩因为place_piece内部用列表推导复制如果不接返回值棋盘永远停在初始状态。3.3 胜负检测单次落子后只需查四条线胜负判断最鲁棒的办法是每下一次就全盘扫描但那样做了很多无用功。高效做法是只查最后落子位置所在的行、列、主对角线、副对角线四条线各向两端延伸数连续同色棋子数。DIRS [(1, 0), (0, 1), (1, 1), (1, -1)] def check_win(board, r, c): 在(r,c)落子后检查是否形成五连, 返回玩家号或0 player board[r][c] for dr, dc in DIRS: count 1 # 正方向 nr, nc r dr, c dc while 0 nr SIZE and 0 nc SIZE and board[nr][nc] player: count 1 nr dr nc dc # 负方向 nr, nc r - dr, c - dc while 0 nr SIZE and 0 nc SIZE and board[nr][nc] player: count 1 nr - dr nc - dc if count 5: return player return 0这段代码里的DIRS定义了四个方向向量水平、垂直、两条对角线。每个方向先往正方向数再往反方向数加起来等于连续同色棋子总数。注意对角线使用(1, 1)和(1, -1)配合循环里的边界判断数组下边不会越界。这个函数在人人对战里放在落子切换玩家之前调用一旦返回非0值就可以弹窗或打印胜者。4. 人机对战核心权值表与评估函数设计人机对战的AI并不需要真的像AlphaGo那样做蒙特卡洛树搜索。对于15×15标准棋盘和结课报告的演示需求一个基于手工权值表的评分算法就能下出像模像样的棋。它的核心思想是对棋盘上每个空位分别从四个方向统计黑白子的各种连子模式给每个模式打一个经验分数最后所有方向得分之和就是该空位的总评分AI选择得分最高的空位落子。4.1 为什么不直接跑极大极小搜索五子棋的分支因子在开局阶段超过30深度到4层时节点数就超过80万用Python做纯递归搜索非常吃力。权值法的复杂度是O(棋盘空位数×方向数×方向长度)只有几千次操作一秒钟能算很多遍。它不保证走出理论上最优解但在结课设计中有两个直接好处第一计算快界面不会卡顿第二权值表可以对外开放你改一个数字就能改变AI的攻防风格写报告时能顺带做一点“参数对比实验”。4.2 模式与权值对应表AI判断的核心是把一段方向上的连续子序列映射成得分。先列出常用的模式作为基准权值需要根据实际对局微调模式含义权值建议五连已有5个己方棋子100000活四两端均开放50000冲四一端被堵10000活三可成活四的形势5000眠三只能成冲四1000活二两端开放500眠二一端受堵100这个表的直观含义是AI优先成五连和堵对手的五连其次是创造活四再到活三。权值差距拉开得越大AI越“贪”会不惜一切代价追求连子。4.3 方向扫描与评分数值化实现时我习惯写一个evaluate_position函数它对每个空位调用score_point然后让AI选择得分最高的点。下面是关键逻辑# 五子棋【人机对战】.py from checkerboard import SIZE # 不同连子模式对应分数字典 SCORES { (1, True): 100000, # 5连 (4, True): 50000, # 活四 (4, False): 10000, # 冲四 (3, True): 5000, (3, False): 1000, (2, True): 500, (2, False): 100, } def count_pattern(seq): seq是一维列表, 统计当前玩家的连子数以及两端是否开放 返回 (连续数, 开放端数量) pass # 实现略, 核心是连续扫描 def score_point(board, row, col, ai_player): 计算空位(row,col)放ai_player棋子后的总分 if board[row][col] ! 0: return -1 total 0 for dr, dc in [(1,0),(0,1),(1,1),(1,-1)]: line [] for step in range(1, 6): nr, nc row dr * step, col dc * step if 0 nr SIZE and 0 nc SIZE: line.append(board[nr][nc]) else: break for step in range(1, 6): nr, nc row - dr * step, col - dc * step if 0 nr SIZE and 0 nc SIZE: line.insert(0, board[nr][nc]) else: break # 在line末尾加一个0表示当前空位, 再统计模式 line.append(0) cnt, open_ends analyze_line(line, ai_player) key (cnt, open_ends 2) total SCORES.get(key, 0) return totalscore_point里用step控制扫描半径到5因为超过5的连续对判断五连没有意义只需要看落子点周围10格距离内的信息。analyze_line会从当前空位出发把左右两边的同色棋子数连在一起再检查两端是否被己方棋子或边界阻挡。如果两端都开放open_ends就是2对应活模式如果只有一端开放则是眠模式。AI选点的完整过程是遍历棋盘全部空位调用score_point把结果按从大到小排序。但若每次都全盘扫描虽然只有225个点再加上方向分析性能也够可视觉上会有一点顿挫。我一般会做一个小优化只扫描已有棋子周围半径2以内的空位。这既保留候选质量又把计算量降到原来的十分之一。def ai_move(board, ai_player): best_score -1 best_pos (SIZE // 2, SIZE // 2) for r in range(SIZE): for c in range(SIZE): if board[r][c] ! 0: continue score score_point(board, r, c, ai_player) if score best_score: best_score score best_pos (r, c) return best_pos这个实现是一个纯进攻型AI它只会找自己得分最高的点不会主动防御。实际对战你会发现AI经常只顾自己连子对手快赢了也不管。所以标准的做法是在评分时同时计算对手在当前空位落子的得分并把它加到AI自己的评分里。总得分 AI得分 对手得分 * 防御系数防御系数取0.9~1.0时就变成攻守兼备取0.8时偏攻击取1.2时偏防守。在报告里展示算法时把防御系数当作变量对比不同系数下的胜负结果就是一个很好的数据分析案例比单纯贴代码更有说服力。5. 结课报告的图表呈现与运行排错技巧报告一般分五章程序思路、设计方案、源程序代码、程序运行、结语。前几章靠文字和代码就能撑起来关键是“程序运行”这一章不能让老师只看控制台截图最好在pygame窗口里截取不同的对局阶段抠出黑白子交替的画面。我建议每张截图下面加一句话说明当前局面是AI处于进攻还是防守这样评审老师能一眼看出算法的工作状态。关于代码和文档的对应有个很简单的技巧在Word里把关键函数名用等宽字体标出比如score_point、check_win然后在源程序代码章节里同样用等宽字体这样读起来非常舒服。如果还想加一点数据分析味道可以用matplotlib把AI不同防御系数下的50局模拟结果画成柱状图横轴是系数纵轴是胜率虽然这超出了原代码的范围但属于对资源的合理扩展。运行环境上最常见的坑是pygame没装上。给结课报告写“环境依赖”时直接写pip install pygame即可。如果你用的是新版Python偶发出现No module named pygame通常是没有在当前的虚拟环境里安装。注意别把虚拟环境目录一起压缩进提交包老师解压后看到一大堆venv反而扣分。另一个容易出错的地方是中文文件名比如“五子棋【人机对战】.py”在Linux或Mac下如果编码不对会抛SyntaxError稳妥做法是把代码文件都改成英文名报告里再写清对应关系。如果程序启动后窗口能打开但点棋子没反应八成是pygame.event.get()的事件类型检查写错。很多人会写成if event.type pygame.MOUSEBUTTONDOWN:而不检查event.button结果把鼠标中键也当成落子在Windows上有时会误触。这里建议统一用event.button 1限定左键。最后提一个写结语时能拉开层次的做法不要简单说“我掌握了Python编程”可以写“通过五子棋项目我完成了从二维数组状态管理到启发式评估函数的闭环实践”并把棋盘建模比作数据清洗AI评分比作特征工程落子决策比作模型预测。这套类比放在数据分析类课程的答辩里高分和面试印象都会更好。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进