7. AI 对手(极小极大搜索)
2026/4/13大约 9 分钟
7. AI 对手:让电脑学会下棋
7.1 AI 下棋的原理
想象你和朋友下棋,每走一步之前都会想:"我走这里,他会怎么应对?他应对之后,我又该怎么走?"
这就是 AI 下棋的核心思路——往未来多看几步,然后选一个对自己最有利的走法。
AI 思考过程:
我走 A → 对手可能走 a1/a2/a3
↓
如果走 a1 → 我可以走 A1/A2/A3(哪个最好?)
如果走 a2 → 我可以走 B1/B2/B3(哪个最好?)
如果走 a3 → 我可以走 C1/C2/C3(哪个最好?)
↓
对手会选对他最好的,我要假设他是最聪明的
↓
所以我要选"即使对手最聪明,我也能接受"的那步7.1.1 为什么不能穷举所有走法?
象棋的局面可能性太多了:
| 搜索深度 | 大约局面数 | 说明 |
|---|---|---|
| 1 步 | ~40 | 只看当前一步 |
| 2 步 | ~1,600 | 我一步+对手一步 |
| 3 步 | ~64,000 | 各走一步半 |
| 4 步 | ~2,560,000 | 各走两步 |
| 5 步 | ~1 亿 | 已经很慢了 |
| 6 步 | ~40 亿 | 普通电脑扛不住 |
核心思路
我们不需要看到最后一步,只需要往未来看 3~5 步,然后用一个"评分函数"判断局面好坏就够了。
7.2 极小极大算法(Minimax)
7.2.1 基本概念
Minimax 算法就像两个玩家在"博弈":
- 我方(MAX):每一步都选分数最高的走法(对自己最有利)
- 对手(MIN):每一步都选分数最低的走法(对我最不利,对他最有利)
我的回合(MAX)
/ | \
走法A 走法B 走法C
/ | \
对手回合 对手回合 对手回合
(MIN) (MIN) (MIN)
/ \ / \ / \
a1 a2 b1 b2 c1 c2
5 3 8 1 4 9
↑ ↑ ↑
选最小=3 选最小=1 选最小=4
↑ ↑ ↑
─────────────────────────────
我选最大=4(走法C)为什么对手要选最小的?
因为分数是从"我方"的角度评的。分数越高对我越有利,对手当然会选让我最不利的走法。
7.2.2 代码实现
C#
/// <summary>
/// 极小极大算法 —— AI 下棋的核心大脑
/// </summary>
/// <param name="board">当前棋盘</param>
/// <param name="depth">还能搜索几步</param>
/// <param name="isMaximizing">当前是不是 AI 的回合</param>
/// <param name="alpha">Alpha 剪枝参数</param>
/// <param name="beta">Beta 剪枝参数</param>
/// <returns>当前局面的评分</returns>
public int Minimax(Piece3D.PieceColor[,] board, int depth, bool isMaximizing,
int alpha = int.MinValue, int beta = int.MaxValue)
{
// 终止条件:搜索到底了,或者有人赢了
if (depth == 0 || IsGameOver(board))
{
return EvaluateBoard(board);
}
if (isMaximizing)
{
// AI 回合:选最大值
int maxEval = int.MinValue;
// 遍历所有可能的走法
foreach (var move in GetAllMoves(board, PieceColor.Red))
{
// 模拟走这一步
var captured = board[move.ToCol, move.ToRow];
board[move.ToCol, move.ToRow] = board[move.FromCol, move.FromRow];
board[move.FromCol, move.FromRow] = null;
// 递归搜索对手的回应
int eval = Minimax(board, depth - 1, false, alpha, beta);
// 撤销这一步(恢复棋盘)
board[move.FromCol, move.FromRow] = board[move.ToCol, move.ToRow];
board[move.ToCol, move.ToRow] = captured;
// 记录最大值
maxEval = Math.Max(maxEval, eval);
// Alpha 剪枝
alpha = Math.Max(alpha, eval);
if (beta <= alpha) break;
}
return maxEval;
}
else
{
// 对手回合:选最小值
int minEval = int.MaxValue;
foreach (var move in GetAllMoves(board, PieceColor.Black))
{
var captured = board[move.ToCol, move.ToRow];
board[move.ToCol, move.ToRow] = board[move.FromCol, move.FromRow];
board[move.FromCol, move.FromRow] = null;
int eval = Minimax(board, depth - 1, true, alpha, beta);
board[move.FromCol, move.FromRow] = board[move.ToCol, move.ToRow];
board[move.ToCol, move.ToRow] = captured;
minEval = Math.Min(minEval, eval);
// Beta 剪枝
beta = Math.Min(beta, eval);
if (beta <= alpha) break;
}
return minEval;
}
}GDScript
## 极小极大算法 —— AI 下棋的核心大脑
func minimax(board: Array, depth: int, is_maximizing: bool,
alpha: int = -999999, beta: int = 999999) -> int:
# 终止条件:搜索到底了,或者有人赢了
if depth == 0 or _is_game_over(board):
return _evaluate_board(board)
if is_maximizing:
# AI 回合:选最大值
var max_eval = -999999
# 遍历所有可能的走法
var moves = _get_all_moves(board, PieceColor.RED)
for move in moves:
# 模拟走这一步
var captured = board[move.to_col][move.to_row]
board[move.to_col][move.to_row] = board[move.from_col][move.from_row]
board[move.from_col][move.from_row] = null
# 递归搜索对手的回应
var eval_score = minimax(board, depth - 1, false, alpha, beta)
# 撤销这一步(恢复棋盘)
board[move.from_col][move.from_row] = board[move.to_col][move.to_row]
board[move.to_col][move.to_row] = captured
# 记录最大值
max_eval = max(max_eval, eval_score)
# Alpha 剪枝
alpha = max(alpha, eval_score)
if beta <= alpha:
break
return max_eval
else:
# 对手回合:选最小值
var min_eval = 999999
var moves = _get_all_moves(board, PieceColor.BLACK)
for move in moves:
var captured = board[move.to_col][move.to_row]
board[move.to_col][move.to_row] = board[move.from_col][move.from_row]
board[move.from_col][move.from_row] = null
var eval_score = minimax(board, depth - 1, true, alpha, beta)
board[move.from_col][move.from_row] = board[move.to_col][move.to_row]
board[move.to_col][move.to_row] = captured
min_eval = min(min_eval, eval_score)
# Beta 剪枝
beta = min(beta, eval_score)
if beta <= alpha:
break
return min_eval7.3 Alpha-Beta 剪枝:让 AI 思考更快
7.3.1 什么是剪枝?
想象你在找最大的苹果,已经找到了一个很大的。这时候看到一棵树上的苹果明显比手里的小,就不用再去看了——这就是剪枝。
没有剪枝:每个分支都要算完(慢!)
40 × 40 × 40 × 40 = 2,560,000 次计算(搜索4步)
有剪枝:很多分支可以跳过(快!)
大约 40 × 20 × 10 × 5 = 40,000 次计算(快了60倍以上!)剪枝的原理
- Alpha:我方已经能保证得到的"最低分"(已知的最小上界)
- Beta:对手已经能保证让我得到的"最高分"(已知的最大下界)
- 当
alpha >= beta时,说明这条路线对手不会同意走,直接跳过
7.3.2 剪枝效果对比
| 搜索深度 | 无剪枝 | 有 Alpha-Beta 剪枝 | 加速比 |
|---|---|---|---|
| 2 步 | 1,600 | ~800 | 2x |
| 4 步 | 2,560,000 | ~40,000 | 64x |
| 6 步 | 40 亿 | ~1,000 万 | 400x |
7.4 评估函数:教 AI 判断局面好坏
AI 往未来看了几步之后,需要一个"评分标准"来判断当前局面谁占优。这个评分标准就是评估函数。
7.4.1 最简单的评估——数子力
每种棋子都有一个"价值":
| 棋子 | 价值 | 说明 |
|---|---|---|
| 帅/将 | 10000 | 被吃了就输了 |
| 车 | 900 | 最强的进攻棋子 |
| 马 | 400 | 灵活但会被蹩腿 |
| 炮 | 450 | 远程攻击能力强 |
| 士 | 200 | 防守棋子 |
| 相/象 | 200 | 防守棋子 |
| 兵/卒 | 100(过河后 200) | 过河兵价值翻倍 |
7.4.2 更好的评估——位置加成
棋子在不同位置价值不同。比如车在中间比在角落好用,兵过了河比没过河强。
C#
/// <summary>
/// 评估整个棋盘局面(从 AI 角度,正值=AI 占优)
/// </summary>
public int EvaluateBoard(Piece3D.PieceColor[,] board)
{
int score = 0;
// 棋子价值表
int[] pieceValues = {
10000, // 将/帅
900, // 车
450, // 炮
400, // 马
200, // 士
200, // 象/相
100 // 兵/卒
};
// 遍历整个棋盘
for (int row = 0; row < 10; row++)
{
for (int col = 0; col < 9; col++)
{
var piece = board[col, row];
if (piece == null) continue;
int value = pieceValues[(int)piece.Type];
// 位置加成:棋子在中心位置加分
float positionBonus = GetPositionBonus(piece.Type, col, row);
value += (int)positionBonus;
// 兵/卒过河加分
if (piece.Type == PieceType.Pawn)
{
if (piece.Color == PieceColor.Red && row <= 4)
value += 100; // 红兵过河
else if (piece.Color == PieceColor.Black && row >= 5)
value += 100; // 黑卒过河
}
// AI 是红方,所以红方加正分,黑方加负分
score += (piece.Color == PieceColor.Red) ? value : -value;
}
}
return score;
}
/// <summary>
/// 获取棋子的位置加成分数
/// </summary>
private float GetPositionBonus(PieceType type, int col, int row)
{
// 车在中间列更灵活
if (type == PieceType.Rook)
{
return (4 - Math.Abs(col - 4)) * 5;
}
// 马在中心更灵活
if (type == PieceType.Knight)
{
int centerDist = Math.Abs(col - 4) + Math.Abs(row - 5);
return Math.Max(0, 10 - centerDist * 2);
}
return 0;
}GDScript
## 评估整个棋盘局面(从 AI 角度,正值=AI 占优)
func _evaluate_board(board: Array) -> int:
var score := 0
# 棋子价值表
var piece_values := {
PieceType.KING: 10000,
PieceType.ROOK: 900,
PieceType.CANNON: 450,
PieceType.KNIGHT: 400,
PieceType.ADVISOR: 200,
PieceType.ELEPHANT: 200,
PieceType.PAWN: 100,
}
# 遍历整个棋盘
for row in range(10):
for col in range(9):
var piece = board[col][row]
if piece == null:
continue
var value: int = piece_values[piece.type]
# 位置加成
var position_bonus: float = _get_position_bonus(piece.type, col, row)
value += int(position_bonus)
# 兵/卒过河加分
if piece.type == PieceType.PAWN:
if piece.color == PieceColor.RED and row <= 4:
value += 100
elif piece.color == PieceColor.BLACK and row >= 5:
value += 100
# AI 是红方,红方加正分,黑方加负分
if piece.color == PieceColor.RED:
score += value
else:
score -= value
return score
## 获取棋子的位置加成分数
func _get_position_bonus(type: PieceType, col: int, row: int) -> float:
if type == PieceType.ROOK:
return (4 - abs(col - 4)) * 5
if type == PieceType.KNIGHT:
var center_dist = abs(col - 4) + abs(row - 5)
return max(0, 10 - center_dist * 2)
return 0.07.5 AI 难度等级
通过调整搜索深度和随机性,实现不同难度的 AI:
| 难度 | 搜索深度 | 随机性 | 特点 |
|---|---|---|---|
| 新手 | 1 步 | 高(30%) | 经常犯错,适合初学者 |
| 简单 | 2 步 | 中(15%) | 偶尔有好棋 |
| 中等 | 3 步 | 低(5%) | 有一定实力 |
| 困难 | 4 步 | 极低(2%) | 大多数人下不过 |
| 大师 | 5 步 | 无(0%) | 很难赢 |
C#
/// <summary>
/// 根据难度等级获取最佳走法
/// </summary>
public Move GetBestMove(Piece3D.PieceColor[,] board, AIDifficulty difficulty)
{
int depth = difficulty switch
{
AIDifficulty.Beginner => 1,
AIDifficulty.Easy => 2,
AIDifficulty.Medium => 3,
AIDifficulty.Hard => 4,
AIDifficulty.Master => 5,
_ => 3
};
float randomChance = difficulty switch
{
AIDifficulty.Beginner => 0.3f,
AIDifficulty.Easy => 0.15f,
AIDifficulty.Medium => 0.05f,
AIDifficulty.Hard => 0.02f,
AIDifficulty.Master => 0f,
_ => 0.05f
};
var allMoves = GetAllMoves(board, PieceColor.Red);
if (allMoves.Count == 0) return null;
// 随机性:有一定概率随机走一步
if (GD.Randf() < randomChance)
{
return allMoves[GD.Randi() % allMoves.Count];
}
// 用 Minimax 找最佳走法
int bestScore = int.MinValue;
Move bestMove = allMoves[0];
foreach (var move in allMoves)
{
var captured = board[move.ToCol, move.ToRow];
board[move.ToCol, move.ToRow] = board[move.FromCol, move.FromRow];
board[move.FromCol, move.FromRow] = null;
int score = Minimax(board, depth - 1, false);
board[move.FromCol, move.FromRow] = board[move.ToCol, move.ToRow];
board[move.ToCol, move.ToRow] = captured;
if (score > bestScore)
{
bestScore = score;
bestMove = move;
}
}
return bestMove;
}GDScript
## 根据难度等级获取最佳走法
func get_best_move(board: Array, difficulty: int) -> Dictionary:
var depth_map := {1: 1, 2: 2, 3: 3, 4: 4, 5: 5}
var random_map := {1: 0.3, 2: 0.15, 3: 0.05, 4: 0.02, 5: 0.0}
var depth: int = depth_map.get(difficulty, 3)
var random_chance: float = random_map.get(difficulty, 0.05)
var all_moves := _get_all_moves(board, PieceColor.RED)
if all_moves.is_empty():
return {}
# 随机性:有一定概率随机走一步
if randf() < random_chance:
return all_moves[randi() % all_moves.size()]
# 用 Minimax 找最佳走法
var best_score := -999999
var best_move: Dictionary = all_moves[0]
for move in all_moves:
var captured = board[move.to_col][move.to_row]
board[move.to_col][move.to_row] = board[move.from_col][move.from_row]
board[move.from_col][move.from_row] = null
var score := minimax(board, depth - 1, false)
board[move.from_col][move.from_row] = board[move.to_col][move.to_row]
board[move.to_col][move.to_row] = captured
if score > best_score:
best_score = score
best_move = move
return best_move7.6 性能优化技巧
7.6.1 走法排序
先搜索"看起来更好"的走法,可以让剪枝效果更好:
走法排序优先级:
1. 吃子的走法(优先搜索,可能触发更多剪枝)
2. 将军的走法
3. 普通走法7.6.2 在后台线程计算
AI 思考时间可能较长,不要阻塞主线程:
C#
/// <summary>
/// 在后台线程中计算 AI 走法
/// </summary>
public async void CalculateMoveAsync()
{
IsThinking = true;
EmitSignal(SignalName.ThinkingStarted);
// 在后台线程计算
var bestMove = await Task.Run(() =>
{
return GetBestMove(currentBoard, currentDifficulty);
});
IsThinking = false;
if (bestMove != null)
{
// 在主线程执行走棋
CallDeferred(MethodName.ExecuteMove, bestMove);
}
EmitSignal(SignalName.ThinkingFinished);
}GDScript
## 在后台线程中计算 AI 走法
func calculate_move_async() -> void:
is_thinking = true
thinking_started.emit()
# 使用 Thread 在后台计算
var thread := Thread.new()
thread.start(_ai_compute_thread.bind(current_board, current_difficulty))
## AI 计算线程函数
func _ai_compute_thread(board: Array, difficulty: int) -> Dictionary:
var result := get_best_move(board, difficulty)
# 回到主线程执行走棋
call_deferred("_on_ai_move_ready", result)
return result
## AI 计算完成后回调
func _on_ai_move_ready(move: Dictionary) -> void:
is_thinking = false
thinking_finished.emit()
if not move.is_empty():
execute_move(move)性能注意
搜索深度越大,计算时间越长。在手机等低性能设备上,建议默认使用 2~3 步深度。
7.7 小结
| 概念 | 一句话解释 |
|---|---|
| Minimax | AI 和对手轮流选最有利的走法 |
| Alpha-Beta 剪枝 | 跳过不可能被选中的分支,大幅加速 |
| 评估函数 | 给棋盘局面打分,帮 AI 判断谁占优 |
| 难度等级 | 通过调整搜索深度和随机性实现 |
→ 8. 网络对战
