共计 3407 个字符,预计需要花费 9 分钟才能阅读完成。
传统决策系统的局限性
在开发棋类游戏 AI 时,很多新手会本能地想到用 if-else 规则硬编码决策逻辑。这种方案在小规模状态空间下勉强可用,但存在两个致命缺陷:

- 状态爆炸问题:象棋平均每个局面有 35 种合法走法,深度为 4 的搜索就需要处理 150 万种局面
- 策略单一性:固定规则无法应对对手的变招,比如只会进攻不会防守的 AI 很容易被引诱入陷阱
我曾用传统方法开发过井字棋 AI,当尝试扩展到五子棋时,if-else判断迅速膨胀到 3000 多行仍然漏洞百出。这促使我转向更系统的解决方案。
算法选型:Minimax vs MCTS
在完全信息博弈领域,有两个主流算法:
- Minimax 算法
- 适用场景:状态空间适中(如象棋、围棋早期)、规则明确的回合制游戏
- 优势:能精确计算最优解,代码结构清晰
-
典型应用:国际象棋引擎 Stockfish 早期版本
-
蒙特卡洛树搜索(MCTS)
- 适用场景:状态空间巨大(如围棋)、存在概率因素的博弈
- 优势:不需要评估函数,通过模拟对局学习
- 典型应用:AlphaGo
对于初学者,建议从 Minimax 入手,因为:
- 递归结构更容易理解博弈树概念
- 评估函数设计能培养对游戏本质的理解
- Claude Code 的模块化风格与 Minimax 天然契合
核心实现:带剪枝的 Minimax
以下是用 Python 3.8 实现的核心代码(以井字棋为例):
from typing import List, Tuple, Optional
import math
# 类型别名提高可读性
Board = List[List[str]]
Move = Tuple[int, int]
class TicTacToeAI:
def __init__(self, player: str = "X", depth: int = 5):
self.player = player
self.opponent = "O" if player == "X" else "X"
self.max_depth = depth # 控制递归深度
def minimax(
self,
board: Board,
depth: int,
alpha: float,
beta: float,
is_maximizing: bool
) -> float:
"""带 Alpha-Beta 剪枝的 Minimax 核心算法"""
# 终止条件检查
if self.is_win(board, self.player):
return 10 - depth # 获胜路径越短分数越高
if self.is_win(board, self.opponent):
return depth - 10 # 阻止对手获胜
if self.is_draw(board) or depth >= self.max_depth:
return 0 # 平局或达到搜索深度
if is_maximizing:
max_eval = -math.inf
for move in self.get_valid_moves(board):
board[move[0]][move[1]] = self.player
current_eval = self.minimax(board, depth + 1, alpha, beta, False)
board[move[0]][move[1]] = " " # 回溯
max_eval = max(max_eval, current_eval)
alpha = max(alpha, current_eval)
if beta <= alpha: # Alpha-Beta 剪枝关键点
break
return max_eval
else:
min_eval = math.inf
for move in self.get_valid_moves(board):
board[move[0]][move[1]] = self.opponent
current_eval = self.minimax(board, depth + 1, alpha, beta, True)
board[move[0]][move[1]] = " " # 回溯
min_eval = min(min_eval, current_eval)
beta = min(beta, current_eval)
if beta <= alpha: # 剪枝条件
break
return min_eval
def find_best_move(self, board: Board) -> Optional[Move]:
"""对外暴露的决策接口"""
best_val = -math.inf
best_move = None
alpha = -math.inf
beta = math.inf
for move in self.get_valid_moves(board):
board[move[0]][move[1]] = self.player
move_val = self.minimax(board, 0, alpha, beta, False)
board[move[0]][move[1]] = " " # 回溯
if move_val > best_val:
best_val = move_val
best_move = move
alpha = max(alpha, move_val)
return best_move
# 辅助方法省略...
关键设计解析:
- 评估函数设计:这里使用简单规则
- 我方胜利:10 – depth(鼓励快速获胜)
- 敌方胜利:depth – 10(延迟失败)
-
平局:0
-
Alpha-Beta 剪枝:
- alpha 记录最大值下界,beta 记录最小值上界
- 当
beta <= alpha时,剩余分支不影响最终结果,可安全剪枝 - 在井字棋测试中,剪枝能减少约 60% 的节点访问
性能优化实战
时间复杂度对比
- 原始 Minimax:O(b^d)
- 带剪枝的 Minimax:最好情况 O(b^(d/2))
实际测试数据(井字棋 d =8):
| 算法类型 | 平均节点访问数 | 耗时(ms) |
|---|---|---|
| 原始 Minimax | 549,946 | 320 |
| Alpha-Beta 剪枝 | 18,297 | 12 |
状态缓存优化
对于更复杂的游戏(如象棋),可以使用 Zobrist Hashing 缓存已计算的状态:
import random
class ZobristHasher:
def __init__(self, size: int = 8):
self.table = [[[random.getrandbits(64) for _ in range(2)]
for _ in range(size)]
for _ in range(size)
]
self.hash = 0
def update(self, row: int, col: int, player: int):
"""玩家落子后更新哈希值"""
self.hash ^= self.table[row][col][player]
常见问题与解决方案
评估函数设计误区
- 权重分配不均
- 错误示例:五子棋中给 ” 四连 ” 赋分 1000,而忽视对手的进攻
-
正确做法:攻守分数平衡,如:
def evaluate(board): my_score = count_lines(board, self.player) opp_score = count_lines(board, self.opponent) return my_score - opp_score * 1.2 # 防守略微优先 -
忽略位置价值
- 在象棋中,中心位置通常价值更高
- 解决方案:添加位置权重矩阵
递归深度控制
- 栈溢出预防:
import sys sys.setrecursionlimit(10000) # 但更好的方法是改用迭代深化搜索 - 实用技巧:
# 迭代深化示例 for depth in range(1, max_depth+1): best_move = find_best_move(board, depth) if time_limit_reached(): break
扩展实践建议
- 迁移到五子棋
- 修改评估函数:加入活三、冲四等棋型判断
-
优化技巧:使用位运算加速棋盘判断
-
并行化改造
from concurrent.futures import ThreadPoolExecutor def parallel_search(moves): with ThreadPoolExecutor() as executor: results = list(executor.map(evaluate_move, moves)) return max(results, key=lambda x: x[1])
经过这个项目的实践,我总结出 AI 开发的三阶段成长路径:
- 先用 Minimax 理解博弈树基本概念
- 通过评估函数设计深入游戏策略本质
- 最后挑战更复杂的 MCTS 算法
建议从井字棋开始,逐步过渡到黑白棋、五子棋等更复杂的游戏,这种渐进式学习能建立扎实的算法思维基础。
正文完
