ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Minmax算法实战:从井字棋到五子棋AI的本地部署

Minmax算法实战:从井字棋到五子棋AI的本地部署 之前接触过一个博弈类项目当时为了给棋类对战加一个“有点水平”的电脑对手我尝试了随机落子、贪心评分、蒙特卡洛模拟效果都不理想。后来把算法换成 Minmax配合 Alpha-Beta 剪枝后AI 的棋力直接从“乱走”提升到“能预判两三步”。最近看到社区里有人在讨论 minmax 以及 minmax h3 本地部署很多初学者对“Minmax 到底是什么、怎么落地、怎么优化”还没建立完整认知。本文就围绕 Minmax 展开从算法原理讲到完整实战再讲如何把 AI 封装成本地可调用的服务全程附上可运行的代码和排错思路。本文适合有 Python 基础、想入门博弈搜索算法或者想在本地服务器/开发机上部署 AI 对战服务的开发者。学完后你可以写出一个井字棋 AI、一个可调搜索深度的五子棋 AI并且能把它们封装成 HTTP 服务供前端或客户端调用。代码均以常见 Python 环境为例版本差异会在文中说明。1. 什么是 Minmax 算法1.1 从博弈场景理解 MinmaxMinmax极大极小算法是博弈论和人工智能中的经典搜索算法常用于零和博弈场景例如井字棋、五子棋、国际象棋、围棋等。所谓“零和博弈”简单说就是一方得分增加另一方必然损失相同分数不存在“双赢”局面。在棋类游戏中Minmax 的核心思想是当前玩家落子时假设对手和自己一样聪明双方都会选择对自己最有利、对对方最不利的一步。于是 AI 在搜索未来的棋局时会交替使用“取最大值”和“取最小值”的思路。轮到 AI 行棋时它从所有候选走法中选择评估分数最高的一步轮到对手行棋时它假设对手会选择让 AI 分数最低的一步。这种“我下一步你下一步”的思考方式本质上是在一棵博弈树上做深度优先搜索。树的根节点是当前棋局每个分支代表一步走法叶子节点代表游戏结束或达到搜索深度上限。Minmax 就是从叶子节点向上回溯逐层计算出每个节点的“理论最优值”。需要注意的是Minmax 假设双方都是理性且信息完全的。如果对手水平不高AI 可能“过度防御”导致它面对弱手时表现并不激进。这是算法本身的特性不是代码 bug。1.2 极大极小值的数学直觉用数学方式描述 Minmax 的话可以这样看假设当前局面是 S玩家 A 是 Max 方玩家 B 是 Min 方。轮到 Max 走时它会选择让评估函数值最大的走法轮到 Min 走时它会选择让评估函数值最小的走法。用伪代码表示就是function minmax(node, depth, maximizingPlayer): if depth 0 or node is terminal: return evaluate(node) if maximizingPlayer: maxValue -infinity for child in node.children(): maxValue max(maxValue, minmax(child, depth-1, False)) return maxValue else: minValue infinity for child in node.children(): minValue min(minValue, minmax(child, depth-1, True)) return minValue这个递归过程会一直进行到游戏结束或达到设定的搜索深度。搜索深度越深AI 的“远见”越强但计算量也越大。如果没有 Alpha-Beta 剪枝等优化手段Minmax 的时间复杂度是 O(b^d)其中 b 是分支因子平均可选走法数d 是搜索深度。1.3 Minmax 的适用边界Minmax 不是万能的。它适合状态空间相对可控、评估函数容易定义的场景。对于井字棋这样棋盘只有 3x3 的游戏Minmax 可以直接搜索到终局AI 能做到不败。对于五子棋棋盘 15x15搜索深度稍微加深计算量就会指数增长必须依赖评估函数和剪枝。对于围棋这类状态空间极大的棋类纯 Minmax 基本不可行通常要结合蒙特卡洛树搜索MCTS和神经网络。因此本文的实战部分会选择井字棋和五子棋两个落地场景让你清晰看到 Minmax 的能力边界在哪里。2. 环境准备与版本说明2.1 运行环境本文示例代码使用 Python 3 编写不依赖第三方游戏引擎。为了让 AI 服务化会用到 Flask。版本需要根据你的实际环境调整本文以常见环境为例重点演示配置思路。项目建议环境操作系统Windows 10/11、Ubuntu 20.04、macOSPython3.8 及以上依赖库Flask 2.x调用测试curl 或 Postman安装 Flask 的命令如下pip install flask如果使用虚拟环境可以这样初始化python -m venv venv # Windows venv\Scripts\activate # Linux/macOS source venv/bin/activate pip install flask2.2 关于 minmax h3 本地部署的说明在社区讨论里“minmax h3 本地部署”中的 H3 并没有统一定义有的场景指轻量级开发设备型号有的场景指某种三层部署结构。本文不依赖任何特定硬件或平台只要你的本机具备 Python 运行环境就能完成演示。如果你手上的设备型号正好叫 H3部署思路完全一致先在设备上安装 Python再把代码放到指定目录最后启动 Flask 服务即可。注意不要因为设备架构是 ARM 或 x86 不同而担心Python 的跨平台特性可以让你直接运行本文代码。2.3 项目结构为了保持代码清晰建议按下面的目录结构组织项目minmax-tutorial/ ├── tictactoe.py # 井字棋 AI 实现 ├── gomoku.py # 五子棋 AI 实现 ├── server.py # Flask 服务入口 └── requirements.txt # 依赖清单requirements.txt 内容如下flask2.3.3后续所有代码都按这个结构编写方便你对照文件路径查找。3. Minmax 算法核心原理拆解3.1 博弈树与胜负评估博弈树是 Minmax 搜索的“地图”。以井字棋为例一盘对局从空棋盘开始双方轮流落子每一步都会产生新的局面。把所有可能局面按先后手关系连接起来就形成了一棵博弈树。树的节点存储棋盘状态边代表一次落子。搜索时我们需要一个评估函数用来给某个局面打分正数表示当前 AI 方占优。负数表示当前 AI 方处于劣势。0 表示势均力敌。如果是终端局面比如 AI 获胜评估分数可以设为 1000对手获胜设为 -1000平局设为 0。如果搜索没有到达终局就需要根据棋子分布设计一个“启发式评估”在 5.1 节会展开。3.2 递归实现 Minmax了解原理后先来看一个最基础的 Minmax 递归实现。这个版本不包含剪枝逻辑最直白适合理解算法骨架。# 文件路径minmax-tutorial/minmax_basic.py import math def minmax(board, depth, is_maximizing): board 是当前局面列表表示 is_maximizing 为 True 表示当前轮到 Max 方。 winner check_winner(board) if winner X: return 10 - depth elif winner O: return depth - 10 elif is_full(board): return 0 if is_maximizing: best -math.inf for move in empty_cells(board): board[move] X best max(best, minmax(board, depth 1, False)) board[move] None return best else: best math.inf for move in empty_cells(board): board[move] O best min(best, minmax(board, depth 1, True)) board[move] None return best这里使用深度因子调整胜负分数意味着越快获胜的路径分数越高越快输掉的路径分数越低。下面解释几个关键点check_winner(board)判断是否有人获胜。is_full(board)判断棋盘是否下满用于识别平局。board[move] None是回溯操作恢复棋盘状态让下一次递归搜索不受干扰。3.3 负极大值 Negamax 简化实现Minmax 的代码需要区分 Max 方和 Min 方逻辑有些冗余。Negamax负极大值算法利用“一方的最优值等于另一方最优值的相反数”的性质用同一套逻辑处理双方节点让代码更简洁。# 文件路径minmax-tutorial/negamax_basic.py import math def negamax(board, depth, player): winner check_winner(board) if winner player: return 10 - depth elif winner opponent(player): return depth - 10 elif is_full(board): return 0 best -math.inf for move in empty_cells(board): board[move] player value -negamax(board, depth 1, opponent(player)) board[move] None best max(best, value) return bestNegamax 代码比 Minmax 更短但需要理解“负号取反”的含义对对手有利的局面对当前玩家一定是不利的所以取负值即可。3.4 Alpha-Beta 剪枝优化Minmax 最严重的问题是效率低。井字棋还好五子棋每层可能有几十个分支搜索深度稍微加深就会卡死。Alpha-Beta 剪枝能在不影响最终结果的前提下剪掉大量“没必要搜索”的分支。剪枝思路可以这样记忆Alpha 表示 Max 方当前已经确保的最低分数。Beta 表示 Min 方当前已经确保的最高分数。在搜索过程中如果某个节点的分数已经比 Alpha 更差Max 方不会选择它如果比 Beta 更好Min 方不会允许它出现。一旦出现 Alpha Beta就可以停止搜索当前节点。# 文件路径minmax-tutorial/alphabeta.py import math def alphabeta(board, depth, alpha, beta, is_maximizing): winner check_winner(board) if winner X: return 10 - depth elif winner O: return depth - 10 elif is_full(board): return 0 if is_maximizing: best -math.inf for move in empty_cells(board): board[move] X best max(best, alphabeta(board, depth 1, alpha, beta, False)) board[move] None alpha max(alpha, best) if beta alpha: break return best else: best math.inf for move in empty_cells(board): board[move] O best min(best, alphabeta(board, depth 1, alpha, beta, True)) board[move] None beta min(beta, best) if beta alpha: break return bestAlpha-Beta 剪枝的搜索效率与节点排列顺序强相关。如果优先搜索“比较好的走法”剪枝效果会更明显这一点在 8.2 节会继续讨论。4. 实战井字棋 AI 的完整实现4.1 棋盘状态设计与胜负判断井字棋棋盘是一个包含 9 个位置的列表索引 0-8 对应 3x3 棋盘的九个格子。玩家为 “X”AI 为 “O”空格用 None 表示。# 文件路径minmax-tutorial/tictactoe.py import math def check_winner(board): lines [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ] for a, b, c in lines: if board[a] is not None and board[a] board[b] board[c]: return board[a] return None def is_full(board): return all(cell is not None for cell in board) def empty_cells(board): return [i for i, cell in enumerate(board) if cell is None]这里的胜负判断覆盖了所有横、竖、对角线的三连情况。is_full用 all() 判断所有格子是否非空逻辑简洁。4.2 评估函数评估函数的作用是把局面转换为数值。井字棋可以直接用胜负结果作为评估值因为搜索深度到达终端局面时结果已经确定。在递归函数中设定AI“O”获胜返回depth - 10越小越好这里要注意我们最终取最小值还是最大值取决于谁在搜索。玩家“X”获胜返回10 - depth。平局返回 0。由于搜索函数使用统一的分数方向我们直接把谁轮到谁走作为参数传入确保 AI 会尽量选择“自己获胜”且“越快越好”的路径。4.3 Minmax 搜索核心代码这里采用 Negamax 风格实现便于简化双方逻辑# 文件路径minmax-tutorial/tictactoe.py def opponent(player): return X if player O else O def evaluate(board, depth, player): winner check_winner(board) if winner player: return 10 - depth elif winner opponent(player): return depth - 10 return 0 def negamax(board, depth, player, alpha, beta): score evaluate(board, depth, player) if score ! 0 or is_full(board): return score best -math.inf for move in empty_cells(board): board[move] player value -negamax(board, depth 1, opponent(player), -beta, -alpha) board[move] None best max(best, value) alpha max(alpha, value) if alpha beta: break return best这里把 Alpha-Beta 剪枝与 Negamax 合在了一起。注意在递归调用时alpha和beta要取相反数并交换位置这是 Negamax 写剪枝的常用方式。4.4 人机对战入口为了让玩家和 AI 对战需要实现一个函数来选择 AI 的最佳落子位置。# 文件路径minmax-tutorial/tictactoe.py def best_move(board, player): best_score -math.inf move None for cell in empty_cells(board): board[cell] player score -negamax(board, 1, opponent(player), -math.inf, math.inf) board[cell] None if score best_score: best_score score move cell return move def print_board(board): symbols [cell if cell is not None else for cell in board] for row in range(3): print(|.join(symbols[row * 3: row * 3 3])) if row ! 2: print(-----) def play_game(): board [None] * 9 human X ai O current human while True: print_board(board) if check_winner(board): print(获胜方:, check_winner(board)) break if is_full(board): print(平局) break if current human: try: cell int(input(请输入落子位置(0-8): )) if board[cell] is not None: print(该位置已有棋子请重新输入) continue board[cell] human except (ValueError, IndexError): print(输入不合法请输入 0-8 之间的整数) continue else: cell best_move(board, ai) board[cell] ai print(AI 落子:, cell) current opponent(current) if __name__ __main__: play_game()4.5 运行与验证在项目目录下运行python tictactoe.py效果如下| | ----- | | ----- | | 请输入落子位置(0-8): 0 AI 落子: 4 X| | ----- |O| ----- | |AI 会选择中心位置这是井字棋中最优的开局应对方式。如果你继续测试会发现无论如何都不会输最多平局。这是因为井字棋的状态空间很小Minmax 配合 Alpha-Beta 剪枝可以在毫秒级完成全深度搜索。5. 升级实战五子棋 AI 与评估函数5.1 五子棋评估思路五子棋的棋盘比井字棋大得多15x15 棋盘有 225 个交叉点不可能搜索到终局。因此需要设计一个启发式评估函数在搜索深度受限时对局面打分。常见做法是“连线评分”对每个位置分别检查四个方向横、竖、两个对角线统计以该位置为起点的连续同色棋子数量以及两端是否被堵住。比如活四两端都开放的四个连续棋子评分最高。冲四一端被堵的四个连续棋子评分很高。活三两端都开放的三个连续棋子评分较高。眠三一端被堵的三个连续棋子评分较低。评估函数会计算 AI 所有方向的分数总和再减去对手所有方向的分数总和得到一个相对优势值。这种“自己进攻分数减去对手进攻分数”的思路能让 AI 既会进攻又会防守。5.2 搜索层数与性能五子棋每个节点的平均可选分支数大约有几十个如果搜索深度设为 4计算量可能在数万到数十万级别配合 Alpha-Beta 剪枝单步可在可接受时间内完成。建议先把搜索深度设为 2验证 AI 是否具备基本防守能力再逐步增加到 4。如果发现响应太慢可以优先优化走法排序把评估分数高的走法排在前面搜索剪枝效率会显著提升。5.3 核心代码下面是一个简化版五子棋 AI 的核心代码重点展示评估函数和搜索框架。# 文件路径minmax-tutorial/gomoku.py import math SIZE 15 EMPTY 0 BLACK 1 # AI WHITE 2 # 玩家 def init_board(): return [[EMPTY for _ in range(SIZE)] for _ in range(SIZE)] def in_board(x, y): return 0 x SIZE and 0 y SIZE def get_line_score(board, x, y, dx, dy, player): count 1 blocked 0 for sign in (1, -1): for step in range(1, 5): nx, ny x sign * step * dx, y sign * step * dy if not in_board(nx, ny): blocked 1 break if board[nx][ny] player: count 1 elif board[nx][ny] EMPTY: break else: blocked 1 break return count, blocked def evaluate_position(board, x, y, player): score 0 opponent_player WHITE if player BLACK else BLACK for dx, dy in [(1, 0), (0, 1), (1, 1), (1, -1)]: count, blocked get_line_score(board, x, y, dx, dy, player) if blocked 0: if count 5: score 10000 elif count 4: score 1000 elif count 3: score 100 elif count 2: score 10 else: if count 5: score 8000 elif count 4: score 500 elif count 3: score 50 # 对手同样位置的威胁AI 需要防守 count2, blocked2 get_line_score(board, x, y, dx, dy, opponent_player) if blocked2 0: if count2 4: score - 9000 elif count2 3: score - 500 else: if count2 4: score - 4000 return score def evaluate_board(board, player): total 0 for x in range(SIZE): for y in range(SIZE): if board[x][y] EMPTY: total evaluate_position(board, x, y, player) return total这里把 AI 的进攻威胁和对手的防守威胁都纳入评估避免 AI 只进攻不防守。搜索函数与井字棋类似只是把empty_cells替换为“候选落子列表”。为了减少搜索范围通常只考虑已有棋子周围 2 格内的空位。# 文件路径minmax-tutorial/gomoku.py def get_candidates(board): candidates set() for x in range(SIZE): for y in range(SIZE): if board[x][y] ! EMPTY: for dx in (-1, 0, 1): for dy in (-1, 0, 1): nx, ny x dx, y dy if in_board(nx, ny) and board[nx][ny] EMPTY: candidates.add((nx, ny)) if not candidates: return [(SIZE // 2, SIZE // 2)] return list(candidates) def negamax_gomoku(board, depth, player, alpha, beta): if depth 0: return evaluate_board(board, player) candidates get_candidates(board) # 走法排序优先评估分数高的走法 scored_moves [] for x, y in candidates: board[x][y] player score evaluate_position(board, x, y, player) board[x][y] EMPTY scored_moves.append((score, x, y)) scored_moves.sort(reverseTrue) best -math.inf for _, x, y in scored_moves[:20]: board[x][y] player value -negamax_gomoku(board, depth - 1, WHITE if player BLACK else BLACK, -beta, -alpha) board[x][y] EMPTY best max(best, value) alpha max(alpha, value) if alpha beta: break return best5.4 运行效果如果你在本地执行主循环AI 会优先占据中心或靠近已有棋子的位置。搜索深度为 2 时AI 能挡住明显的三连深度为 4 时AI 会主动创造“双活三”之类的杀招。实际项目里可以根据机器性能动态调整深度。6. 把 Minmax AI 本地部署为 HTTP 服务6.1 为什么封装成服务棋类 AI 通常是独立程序但如果要嵌入 Web 前端、小程序或游戏客户端更好的方式是封装成 HTTP 服务。前端只负责显示棋盘和收集玩家操作AI 决策交给后端这样算法逻辑可以复用也能方便地扩展成多人对战。本地部署服务的另一个好处是无需把模型或算法代码暴露给前端。敏感的业务逻辑、评估函数、搜索参数都可以留在服务端客户端只拿到“推荐落子位置”这个结果。6.2 Flask 服务代码下面用一个 Flask 服务封装五子棋 AI。客户端只需要提交当前棋盘状态和 AI 执子颜色服务端返回落子坐标。# 文件路径minmax-tutorial/server.py from flask import Flask, request, jsonify from gomoku import init_board, get_candidates, negamax_gomoku, evaluate_board app Flask(__name__) SEARCH_DEPTH 2 def convert_board(data): board init_board() for cell in data: x int(cell[x]) y int(cell[y]) value int(cell[value]) board[x][y] value return board app.route(/api/bestmove, methods[POST]) def best_move(): data request.get_json() board convert_board(data[board]) player int(data[player]) candidates get_candidates(board) if not candidates: return jsonify({error: 棋盘已满}), 400 best_score float(-inf) best_move_pos None alpha float(-inf) beta float(inf) opponent_player 2 if player 1 else 1 scored_moves [] for x, y in candidates: board[x][y] player score evaluate_board(board, player) board[x][y] 0 scored_moves.append((score, x, y)) scored_moves.sort(reverseTrue) for _, x, y in scored_moves[:20]: board[x][y] player score -negamax_gomoku(board, SEARCH_DEPTH - 1, opponent_player, -beta, -alpha) board[x][y] 0 if score best_score: best_score score best_move_pos (x, y) alpha max(alpha, best_score) if best_move_pos is None: return jsonify({error: 没有可用落子}), 500 return jsonify({x: best_move_pos[0], y: best_move_pos[1], score: best_score}) if __name__ __main__: app.run(host0.0.0.0, port8000, debugFalse)6.3 启动与调用启动服务python server.py用 curl 测试curl -X POST http://127.0.0.1:8000/api/bestmove \ -H Content-Type: application/json \ -d {board: [], player: 1}空棋盘时预期返回棋盘中心位置附近的结果{score: 30, x: 7, y: 7}如果前端已经运行到中盘提交的 board 数组会包含已落子的坐标和颜色服务端会返回下一步推荐位置。需要注意host0.0.0.0意味着服务会监听本机所有网络接口。如果设备处于局域网其他机器也能访问。若只是在本地调试改为host127.0.0.1更安全。6.4 在 H3 本地设备上部署的注意事项如果你的 H3 设备是 ARM 架构或性能有限的开发板部署时要注意以下几点确认 Python 版本。部分开发板自带 Python 3.7 或更旧版本建议升级到 3.8避免语法兼容问题。调整搜索深度。性能不足时把SEARCH_DEPTH降为 1 或 2保证响应时间。使用debugFalse避免调试模式在局域网环境下暴露交互式调试器。如果希望在系统启动时自动运行服务可以用 systemd 或 supervisor 托管 Flask 进程避免手动启动。7. 常见问题与排查思路问题现象常见原因解决思路AI 执行速度很慢搜索深度过大、候选走法过多降低搜索深度、限制候选走法数量、优化走法排序AI 总是应对迟缓不主动进攻评估函数中防守权重过高调整进攻分数的权重比例让进攻威胁得分更高服务启动后外部无法访问监听地址不是 0.0.0.0或防火墙拦截修改 host 参数检查防火墙和端口curl 返回 400请求 JSON 格式错误或 board 字段缺失检查 JSON 结构确认board字段是数组服务响应不稳定没有做超时控制请求量过大增加超时参数、限制并发数或使用队列机制落子坐标非法后端未校验棋盘边界在服务端增加in_board校验返回 400 提示如果遇到递归深度报错RecursionError大概率是搜索深度设置过大或者递归终止条件没有覆盖到游戏结束状态。检查is_full和胜负判断是否在所有分支上生效。8. 最佳实践与工程建议8.1 评估函数决定 AI 的上限Minmax 搜索只是“框架”真正决定 AI 棋力上限的是评估函数。建议把评估函数拆成独立的模块方便调整权重和测试。参考的评分策略如下进攻分数和防守分数分开统计。优先考虑“连五”和“活四”这样的直接胜负手。对双方威胁做差让 AI 在进攻与防守之间动态平衡。8.2 走法排序是性能优化的重要杠杆Alpha-Beta 剪枝的效果对走法顺序非常敏感。如果每次先搜索“当前评估分数最高”的走法剪枝可以在很早阶段发生搜索量大幅缩减如果先搜索烂走法剪枝效果会大打折扣。所以每次搜索前对候选位置按评估分数预排序是很有必要的。8.3 服务端必须做输入校验HTTP 服务暴露出去后不能假设客户端传来的数据总是合法。必须校验坐标范围、棋子类型、棋盘长度等否则可能出现越界访问。建议在接口入口统一做一层校验非法输入直接返回 4xx 错误。8.4 安全与最小权限原则如果服务要部署到生产环境注意以下几点不要用 root 用户运行 Flask 服务。监听地址按需绑定仅本地使用时用 127.0.0.1。如果服务需要暴露到公网前面对接 Nginx 等反向代理并做好访问控制。不要在服务端暴露不必要的调试信息。8.5 日志与监控给 AI 服务增加日志是必要的尤其是生产环境。每次请求记录棋盘摘要、搜索深度、响应耗时和返回结果即可。问题出现时能极大缩短排查时间。如果响应时间突然变长优先关注搜索深度或候选走法数量是否异常。9. 总结与下一步学习方向本文从 Minmax 算法的核心思想出发解释了极大极小值、博弈树、评估函数的底层逻辑并用井字棋和五子棋两个实战案例展示了算法落地过程。最后通过 Flask 把五子棋 AI 封装成 HTTP 服务完成了一次典型的本地部署流程。如果你手上有 H3 设备或任何轻量级开发板完全可以按相同思路把服务跑起来。在实际项目中我最想提醒你的一点是不要试图把所有逻辑都塞进搜索函数。评估函数、走法生成、剪枝策略、服务接口应该是四个独立模块这样才能逐步调优、单点排查。下一步可以继续学习的方向包括蒙特卡洛树搜索MCTS适合分支因子更大的棋类游戏。深度学习评估网络用神经网络替代人工评估函数进一步提升棋力。并行搜索在多核设备上并行评估多个候选走法减少响应时间。对局回放与性能分析记录每步搜索耗时和搜索节点数针对性优化。如果你想在现有代码上继续练习建议先调整五子棋评估函数的权重看看 AI 风格会发生什么变化再尝试增加“禁手”等规则这会让你对算法设计和工程实现有更深的理解。
返回列表