拓冰建站拓冰建站
首页 / 资讯中心 / 正文

用Python和Pygame拆解围棋源码:从规则建模到AI落子

简介一款基于 Python 与 Pygame 的围棋小游戏源码包适合 Python 初学者、游戏开发爱好者用于学习事件驱动编程、面向对象设计与基础 AI 算法。项目围绕围棋核心规则展开包含主程序、棋盘逻辑、玩家与 AI 模块可运行可修改有助于理解合法落子判断、胜负判定以及基于 Minimax 的简单电脑决策思路。整个压缩包共 6 个文件以 .py 源码、.md 说明文档、.txt 配置、.gitignore、字体文件和示例图片组成整体仅 695KB结构轻量、便于下载和二次开发。目前已有 1162 人学习下载。通过阅读 main.py、game_board.py、player.py 与 ai.py 等模块读者既能巩固 Python 语法和 Pygame 窗口、事件、图像处理等核心组件用法也能掌握棋盘状态更新、绘制刷新和简单 AI 策略的实际落地方式是快速上手 Python 小游戏开发的实用素材。1. 用 Python 和 Pygame 写围棋为什么值得拆一遍围棋在棋类游戏里是个异类规则只有两条围地和提子但状态复杂度远超国际象棋。用 Python 这种解释型语言配合 Pygame 做 19 路棋盘性能上注定不是对手可我反而觉得这正是它适合当教材的原因。这份 pygo-master 源码包里没有一堆分散的模块核心就是一个 go.py外加 static、img、font 资源目录和 requirement.txt结构非常干净。如果你想在免费 Python 源码大全这类合集里翻出一个能直接跑起来的项目刚按 Python 安装教程配好环境的话这个围棋小游戏会比俄罗斯方块更能逼你处理数据结构、事件循环和简单 AI。我会按实际代码拆开讲包括怎么建模棋盘、怎么提子、怎么在 Pygame 主循环里把逻辑和渲染接起来。2. Pygame 事件驱动与围棋规则建模这章把地基打牢不然直接看 go.py 会被一堆状态变量绕晕。2.1 Display、Surface 与事件循环的关系Pygame 不是游戏引擎而是一组底层库的集合。创建窗口用pygame.display.set_mode()它返回一个 Surface所有绘制都在这个 Surface 上完成。围棋的棋盘是静态网格不需要每帧重新加载图片只需要在落子后重绘变化的部分。环境配置按 Python 安装教程走就行如果用 pip 安装最简单的是pip install pygame。装完后可以用python -c import pygame; print(pygame.version.ver)验证版本。注意 Python 3.10 以下和以上对 pygame 的 wheel 支持不同后面第 5 章会讲。事件循环是 Pygame 的核心模式pygame.event.get()拉取队列里的所有事件比如鼠标点击、键盘按键和窗口关闭。围棋只需要响应鼠标左键但你要分辨是「点击棋盘交叉点」还是「点击界面按钮」所以坐标换算要写对。很多人在这个阶段把事件处理和绘制逻辑混在一个大循环里导致调试时很难定位是规则出错还是渲染出错。2.2 用二维数组表示棋盘状态19 路围棋盘有 361 个交叉点。最简单也最稳的建模是二维数组board[y][x]取值为 0空、1黑、2白。为什么不用一维数组因为判断气的邻居时二维索引更直观调试时把棋盘打印成 19 行数字一目了然。项目正文里没给出具体结构但这类小游戏十有八九是这个方案。BOARD_SIZE 19 EMPTY, BLACK, WHITE 0, 1, 2 def init_board(): # 初始化全空棋盘 return [[EMPTY for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)] def neighbors(x, y): # 四个相邻方向越界跳过 for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)): nx, ny x dx, y dy if 0 nx BOARD_SIZE and 0 ny BOARD_SIZE: yield nx, nyinit_board生成一个全部为空的棋盘。neighbors返回相邻四个方向的坐标不越界。后续计算气、提子、判断合法性都要用到它建议把它作为公共函数。BOARD_SIZE在 go.py 里可以改成 9 或 13做小棋盘测试时更快。2.3 气的计算与提子规则围棋规则中「气」是一个连通块直接相邻的空点。落子后先提对方无气的块再提自己无气的块。没有气的己方落子通常叫自杀多数规则禁止。用集合去重可以避免重复计算同一块棋。def find_group(board, x, y): # 深度优先找同色连通块 color board[y][x] if color EMPTY: return set() group, stack {(x, y)}, [(x, y)] while stack: cx, cy stack.pop() for nx, ny in neighbors(cx, cy): if board[ny][nx] color and (nx, ny) not in group: group.add((nx, ny)) stack.append((nx, ny)) return group def count_liberties(board, group): # 统计一个连通块的气数 liberties set() for x, y in group: for nx, ny in neighbors(x, y): if board[ny][nx] EMPTY: liberties.add((nx, ny)) return len(liberties)find_group用深度优先搜索把同色连通块全部找出来count_liberties数这个块周围有几个空点。提子的时候遍历对方所有连通块气数为 0 就移除。这里有个初学者常踩的坑落子后如果先提对方再判断自己那么自己的相邻块会因为对方被提走而有了新气自杀判断不能只看落子前。下面表格列出 Pygame 中本项目涉及的关键组件方便对照 go.py 里的调用。模块用途go.py 场景display创建窗口、设置标题set_mode((720, 720))event读取鼠标、键盘事件点击落子、按 R 重置draw绘制线段、圆形画棋盘网格和棋子font渲染文字显示对局状态和中文提示image加载图片加载皮肤图片到 Surfacetime控制帧率pygame.time.Clock().tick(30)2.4 屏幕坐标与棋盘坐标的换算Pygame 的鼠标事件返回的是窗口像素坐标要落子必须先换成交叉点坐标。常见做法是固定 margin 和 cell_size用四舍五入找最近交叉点def screen_to_board(pos, margin, cell_size): x, y pos board_x round((x - margin) / cell_size) board_y round((y - margin) / cell_size) if 0 board_x BOARD_SIZE and 0 board_y BOARD_SIZE: return board_x, board_y return None参数说明margin是棋盘四周的留白cell_size是相邻交叉点之间的像素间距。窗口宽度应满足MARGIN*2 CELL_SIZE * (BOARD_SIZE-1)这样棋盘才能居中。换算时用round而不是int避免点击两线中间时永远靠向原点。如果返回None说明点击在棋盘外直接忽略。3. go.py 的结构与主循环实战先别急着跑代码把资源包拆开看。3.1 解压后的文件清单pygo-master 目录下文件不多只有 go.py 作为核心程序static、img、font 三个目录放资源外加 .gitignore、requirement.txt 和 README.md。这种单文件设计的好处是学习成本低坏处是所有函数都堆在一个文件里命名稍乱就要靠搜索跳转。先看表格文件/目录作用go.py游戏主程序入口static/静态配置或辅助资源img/棋盘背景、棋子图片font/中文字体文件.gitignoreGit 忽略规则requirement.txt依赖清单注意依赖文件是 requirement.txt不是常见的 requirements.txt安装时要按实际文件名执行。这种细节在小项目里经常被忽略。运行前先安装依赖pip install -r requirement.txt如果这里只写了pygame那它会拉取当前 Python 环境对应的最新版。建议之后改成pygame2.0Pygame 2 的事件和文本渲染比 1.x 稳定很多。3.2 主循环骨架事件、更新、渲染从文件命名看go.py 承担了主程序Pygame 项目的循环结构通常如下import pygame import sys def main(): pygame.init() screen pygame.display.set_mode((720, 720)) pygame.display.set_caption(Go - Pygame) clock pygame.time.Clock() board init_board() turn BLACK while True: for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() elif event.type pygame.MOUSEBUTTONDOWN and event.button 1: x, y screen_to_board(event.pos, MARGIN, CELL_SIZE) if x is not None: # 在这里调用 try_place_stone pass # 渲染棋盘 clock.tick(30) if __name__ __main__: main()这个骨架是 Pygame 通用写法。pygame.init()初始化所有模块set_mode返回屏幕 Surface。event.button 1表示鼠标左键按下。pygame.QUIT事件必须处理否则点关闭按钮时进程会卡住。clock.tick(30)限制帧率到 30 FPS围棋交互频率不高30 足够反而能降低 CPU 占用。真正要落子时把坐标传给规则函数再更新 board 变量。3.3 用 font 和 image 加载资源围棋界面需要显示对局状态比如「黑方执子」「白方超时」。Pygame 默认字体不含中文字形所以要用 font 目录下的字体文件。常见做法import os font_path os.path.join(font, simhei.ttf) font pygame.font.Font(font_path, 24) text_surface font.render(黑子落子, True, (255, 255, 255)) screen.blit(text_surface, (10, 10))参数说明Font第一个参数是字体路径第二个是字号。render的第三个参数是抗锯齿开关中文小字号建议开 True。如果字体路径错误Pygame 会抛异常可以用pygame.font.get_fonts()先查看系统可用字体。图片资源用pygame.image.load加载加载后调用.convert()可以把像素格式转成屏幕格式blit 时更快。如果图片尺寸和窗口不匹配用pygame.transform.scale缩放。到这里go.py 的骨架已经能跑起来了剩下的核心问题是规则引擎。4. 规则引擎完整实现与 AI 落子这是整个项目最值得读的部分也是围棋和五子棋这类简单棋类最大的区别。4.1 合法落子的完整流程落子前要判断三件事坐标为空落子后提掉对方无气块不自杀。更进一步还要考虑打劫。下面是一个可用的实现def opponent(color): return WHITE if color BLACK else BLACK def try_place_stone(board, x, y, color): if board[y][x] ! EMPTY: return False board[y][x] color # 临时落子 captured 0 # 提子数可用于界面显示 for nx, ny in neighbors(x, y): if board[ny][nx] opponent(color): group find_group(board, nx, ny) if count_liberties(board, group) 0: for gx, gy in group: board[gy][gx] EMPTY captured len(group) my_group find_group(board, x, y) if count_liberties(board, my_group) 0: # 自杀检查撤销落子 board[y][x] EMPTY return False return True代码逻辑先临时落子然后检查四个方向的对手连通块气数为 0 就整块提掉。提完之后再检查落在位置上的自己这块棋还有没有气没有就是自杀撤销落子并返回 False。注意这里先提对方再检查自己非常关键否则会出现「看似自杀实际提了对方之后自己又有气」的漏判。captured变量可以累加用于界面显示双方吃子数。4.2 打劫与历史状态围棋的劫规则单独拎出来说是因为它很容易被忽略。简单实现是记录上一手被提掉的那个点下一次落子禁止下在那里除非能提回更多子。go.py 如果只做单文件教学这个粒度够了。class GameState: def __init__(self): self.board init_board() self.turn BLACK self.ko_point None self.last_capture None类里有board、turn、ko_point和last_capture四个属性。每次成功落子且只提了一子时把被提的位置记入ko_point下一次落子如果目标坐标等于ko_point直接拒绝。但实际围棋中打劫必须「隔一手」才能提回这个简化模型并不完善。更完整的做法是保存上一步棋盘快照回推一步对比当前棋盘是否完全相同。对于 19 路棋盘深拷贝代价并不高毕竟一次对局最多几百手。4.3 终局数子与胜负围棋终局判断很复杂小游戏通常会用简化数子方式。一个可用思路是双方都跳过落子后把每个空区域边界颜色找出来清一色属于黑方就计为黑地清一色属于白方就计为白地边界混合的区域不算任何人的地。def find_region(board, x, y): # 找空区域类似 find_group if board[y][x] ! EMPTY: return set() region, stack {(x, y)}, [(x, y)] while stack: cx, cy stack.pop() for nx, ny in neighbors(cx, cy): if board[ny][nx] EMPTY and (nx, ny) not in region: region.add((nx, ny)) stack.append((nx, ny)) return region def score_board(board): visited [[False] * BOARD_SIZE for _ in range(BOARD_SIZE)] score {BLACK: 0, WHITE: 0} for y in range(BOARD_SIZE): for x in range(BOARD_SIZE): c board[y][x] if c in (BLACK, WHITE): score[c] 1 elif not visited[y][x]: region find_region(board, x, y) borders set() for rx, ry in region: visited[ry][rx] True for nx, ny in neighbors(rx, ry): if board[ny][nx] ! EMPTY: borders.add(board[ny][nx]) if borders {BLACK}: score[BLACK] len(region) elif borders {WHITE}: score[WHITE] len(region) return score[BLACK], score[WHITE]这段代码把棋盘遍历一遍先数棋子再处理空区域。visited保证每个空区域只算一次。注意这种简化不会处理双活在自由对局里可能出现争议但作为学习项目够用。如果想要更严格可以再判断眼位和双活不过那就是单独一篇博文的量了。4.4 一个能用的 AI 落子策略如果 go.py 有 AI 对手大概率不会用蒙特卡洛树搜索因为单文件写不下。最常见的是浅层 Minimax 或者「模拟落子 评估」。评估函数用双方气的差值简单且反应速度快。def evaluate(board, color): opp opponent(color) my_air sum(count_liberties(board, find_group(board, x, y)) for x in range(BOARD_SIZE) for y in range(BOARD_SIZE) if board[y][x] color) opp_air sum(count_liberties(board, find_group(board, x, y)) for x in range(BOARD_SIZE) for y in range(BOARD_SIZE) if board[y][x] opp) return my_air - opp_air def ai_move(board, ai_color): best_score float(-inf) best_move None for x in range(BOARD_SIZE): for y in range(BOARD_SIZE): if board[y][x] ! EMPTY: continue snapshot [row[:] for row in board] if try_place_stone(snapshot, x, y, ai_color): score evaluate(snapshot, ai_color) if score best_score: best_score score best_move (x, y) return best_moveai_move遍历所有空点复制棋盘后模拟落子再用evaluate计算局面分。评估里的count_liberties单独对每个连通块求气然后累加这样等于奖励那些气多的棋型同时惩罚对方的气。因为每次都重新找连通块19 路全盘搜索会比较慢实际跑的时候建议把BOARD_SIZE临时改成 9。如果你希望 AI 更「贪吃」可以把评估函数改成净提子数也就是模拟后captured的差值。评估策略计算方式对局风格净提子数落子后双方累计提子差激进能提就提总气差己方总气减对方总气稳健兼顾逃跑和包围领地估算用数子函数计算地盘差最接近真围棋但慢5. 运行排错与两个实用小技巧这一章不写大道理只说我会在跑这种源码包时碰到的真问题。5.1 安装 pygame 失败与 Python 版本匹配很多人在pip install pygame时遇到error: failed to build pygame when getting requirements to build wheel。这个错误通常是因为当前 Python 版本没有对应平台的预编译 wheel安装流程被迫走源码编译而本机缺少编译工具链。解决办法是升级 pip或者直接用预编译版本pip install --upgrade pip pip install pygame --pre如果还不行就装社区维护的pygame-ce它的包名是 pygame-ce但导入仍是import pygame。在 requirement.txt 里把pygame改成pygame-ce即可。这个资源里写的是 requirement.txt其中依赖通常就是pygame跑不起来时优先检查这一点。5.2 中文字体显示成方块的排查围棋 UI 显示中文时如果全是方块第一件事检查 font/ 目录下的文件是否被 Git 忽略。.gitignore如果写了*.ttf或font/拉下来就没有字体文件。第二件事是路径拼接不要写死/home/user/font/simhei.ttf用相对路径加os.path.join。第三件事是确认字体格式pygame.font.Font支持.ttf和.otf不支持.ttc遇到.ttc需要转换或换字体。5.3 用常量驱动棋盘尺寸让 19 路和 9 路通吃最后是一个我很常用的技巧把棋盘尺寸全部抽成常量而不是写死在所有函数参数里。BOARD_SIZE 19 CELL_SIZE 36 MARGIN 24 WINDOW MARGIN * 2 CELL_SIZE * (BOARD_SIZE - 1)这样做的直接好处是调试ai_move时把BOARD_SIZE改成 9窗口大小、网格间距、坐标换算全部自动跟着变不需要去每个函数里找魔法数字。如果你修改 go.py 想加一个「快速对局」模式只需要在启动时让BOARD_SIZE 9然后重新初始化一遍窗口。这比单独建一个 9 路棋盘类省事得多。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门